93 preguntas de entrevista de C# para contratar a los mejores desarrolladores
Evaluar a los desarrolladores de C# requiere una sólida comprensión del lenguaje y sus matices. A medida que la tecnología evoluciona, es útil tener un conjunto de preguntas al que recurrir, lo que garantiza que esté bien preparado para evaluar a los candidatos en todos los niveles, de forma similar a como hemos discutido las habilidades para un desarrollador de software.
Esta publicación de blog proporciona un banco de preguntas categorizadas por nivel de experiencia, que comienza con preguntas básicas a expertas de C# e incluso algunas preguntas de opción múltiple (MCQ). Utilice estas preguntas para ayudarle a evaluar el conocimiento del candidato sobre los conceptos de C#, las habilidades de codificación y las habilidades de resolución de problemas.
Al utilizar esta lista completa, puede evaluar con confianza a los candidatos y tomar decisiones de contratación informadas. Para optimizar aún más su selección, considere usar las pruebas en línea de C# de Adaface para identificar rápidamente a los mejores talentos antes de la etapa de la entrevista.
Tabla de contenido
Preguntas básicas de la entrevista de C#
Preguntas intermedias de la entrevista de C#
Preguntas avanzadas de la entrevista de C#
Preguntas de la entrevista de experto en C#
C# MCQ
¿Qué habilidades de C# debe evaluar durante la fase de entrevista?
Contrate a desarrolladores de C# capacitados con evaluaciones específicas y entrevistas perspicaces
Descargue la plantilla de preguntas de la entrevista de C# en múltiples formatos
1. ¿Qué es C# y por qué lo usamos?
C# es un lenguaje de programación moderno, orientado a objetos y con seguridad de tipos, desarrollado por Microsoft. Está diseñado para construir una amplia gama de aplicaciones que se ejecutan en la plataforma .NET.
Usamos C# porque:
-
Es versátil: Adecuado para el desarrollo web, de escritorio, móvil, de juegos (Unity) y en la nube.
-
Ofrece herramientas robustas: Visual Studio proporciona un excelente soporte IDE.
-
Tiene una gran comunidad: Hay abundantes recursos y soporte disponibles.
-
Proporciona gestión automática de memoria: A través de la recolección de basura.
-
Ofrece características como LINQ para consultar datos, async/await para programación asíncrona y genéricos para seguridad de tipos. Por ejemplo:
// Ejemplo de un programa simple en C# using System; public class HelloWorld { public static void Main(string[] args) { Console.WriteLine("Hello, World!"); } }
2. Explique la diferencia entre los tipos de valor y los tipos de referencia en C#.
Los tipos de valor y los tipos de referencia difieren en cómo almacenan los datos. Los tipos de valor (como int
, bool
, struct
, enum
) contienen directamente su valor dentro de su memoria asignada. Cuando asigna una variable de tipo de valor a otra, se crea una copia del valor. Cada variable tiene entonces su propia copia independiente.
Los tipos de referencia (como string
, class
, array
, delegate
) almacenan una referencia (una dirección de memoria) a los datos reales, que se encuentra en el montón (heap). Cuando asigna una variable de tipo de referencia, está copiando la referencia, no los datos en sí. Ambas variables apuntarán entonces a la misma ubicación de memoria. Modificar los datos a través de una variable afectará a la otra.
3. ¿Cuál es el propósito de la declaración 'using'?
La declaración using
en C# (y construcciones similares en otros lenguajes como la declaración with
de Python) se utiliza para asegurar que los recursos se liberen adecuadamente, incluso si ocurren excepciones. Proporciona una manera conveniente de llamar automáticamente al método Dispose()
en un objeto cuando sale del ámbito.
Específicamente, la declaración using
funciona con objetos que implementan la interfaz IDisposable
. Automáticamente genera un bloque try...finally
. El objeto se crea y se utiliza dentro del bloque try
, y el método Dispose()
se llama en el bloque finally
, garantizando la limpieza de recursos independientemente de si el código dentro del bloque try
se completa con éxito o lanza una excepción. Por ejemplo:
using (Font font3 = new Font("Arial", 10.0f)) { // Usar font3 } // font3.Dispose() se llama automáticamente aquí
4. Describe el concepto de herencia en C#.
La herencia en C# es un mecanismo donde una nueva clase (clase derivada o hija) adquiere propiedades y métodos de una clase existente (clase base o padre). Esto promueve la reutilización de código y establece una relación jerárquica entre clases. La clase derivada puede heredar miembros (campos, propiedades, métodos, eventos, etc.) de la clase base y también puede agregar nuevos miembros o sobrescribir los heredados, extendiendo o especializando así el comportamiento de la clase base.
Aspectos clave de la herencia incluyen:
- Reutilización de código: Evita el código redundante al heredar de clases existentes.
- Sintaxis
:
: Se utiliza para especificar la herencia (por ejemplo,class Dog : Animal
). virtual
yoverride
: Permite a las clases derivadas modificar el comportamiento de los métodos heredados. El método de la clase base debe declararsevirtual
, y la clase derivada utiliza la palabra claveoverride
.- Palabra clave
base
: Se utiliza para llamar al constructor o método de la clase base desde la clase derivada.
5. ¿Qué son las interfaces en C# y en qué se diferencian de las clases abstractas?
Las interfaces en C# definen un contrato que las clases pueden implementar. Contienen solo declaraciones de métodos, propiedades, eventos e indexadores, pero no implementación. Una clase puede implementar múltiples interfaces.
Las clases abstractas, por otro lado, pueden contener tanto declaraciones como implementaciones. Pueden tener campos, constructores y métodos con cuerpos. Una clase solo puede heredar de una clase abstracta. La diferencia clave es que las interfaces exigen una relación 'puede hacer', mientras que las clases abstractas establecen una relación 'es-un'. Además, las interfaces admiten la herencia múltiple, mientras que las clases abstractas no. Ejemplo:
interface IMyInterface { void MyMethod(); } abstract class MyAbstractClass { public abstract void MyMethod(); }
6. ¿Qué es el polimorfismo y cómo se logra en C#?
El polimorfismo, que significa "muchas formas", es la capacidad de una clase o interfaz para adoptar muchas formas. Permite que los objetos de diferentes clases sean tratados como objetos de un tipo común. En C#, el polimorfismo se logra a través de varios mecanismos:
- Herencia: Las clases derivadas heredan de una clase base y pueden anular métodos virtuales. Este es el polimorfismo en tiempo de ejecución.
- Interfaces: Las clases pueden implementar interfaces, proporcionando diferentes implementaciones para los mismos métodos de interfaz. Esto también facilita el polimorfismo en tiempo de ejecución.
- Sobrecarga de métodos: Crear múltiples métodos con el mismo nombre pero diferentes parámetros dentro de una clase. Este es el polimorfismo en tiempo de compilación.
- Clases abstractas: Las clases abstractas definen métodos abstractos que deben ser implementados por las clases derivadas, lo que impone una cierta estructura y polimorfismo.
7. Explica la diferencia entre '==' y '.Equals()' en C#.
En C#, ==
y .Equals()
se utilizan para comparar valores, pero difieren en su comportamiento, especialmente cuando se trata de tipos de referencia.
==
: Este es el operador de igualdad. Por defecto, para tipos de referencia, verifica la igualdad de referencia, lo que significa que verifica si dos variables apuntan a la misma ubicación de memoria (es decir, ¿son la misma instancia de objeto?). Para los tipos de valor,==
compara los valores de los operandos. Sin embargo, el operador==
puede ser sobrecargado por clases y structs para proporcionar lógica de igualdad personalizada..Equals()
: Este es un método heredado de la claseSystem.Object
. Por defecto, para tipos de referencia, su comportamiento es similar a==
, verificando la igualdad de referencia. Sin embargo, su propósito principal es ser anulado en las clases derivadas para proporcionar igualdad de valor. La igualdad de valor significa que verifica si el contenido de dos objetos es el mismo según los criterios definidos dentro de la clase. Los tipos de valor suelen anular el método.Equals()
para comparar valores y no direcciones de memoria.
8. ¿Qué es un delegado en C#?
En C#, un delegado es un tipo que representa referencias a métodos con una lista de parámetros y un tipo de retorno particulares. Esencialmente, es un puntero a función seguro para tipos. Los delegados te permiten tratar los métodos como objetos, permitiendo que se pasen como argumentos a otros métodos, se almacenen en estructuras de datos y se invoquen dinámicamente.
Piensa en ello como un marcador de posición. Declaras un tipo delegado, luego puedes crear instancias de ese delegado que 'apuntan' a métodos específicos. Estos métodos deben coincidir con la firma (tipos de parámetros y tipo de retorno) definida por el delegado. Los delegados son fundamentales para implementar eventos y mecanismos de devolución de llamada en C#.
9. ¿Qué son los eventos en C# y cómo se usan?
En C#, los eventos son una forma para que una clase u objeto notifique a otras clases u objetos cuando ocurre algo de interés. Se basan en el tipo delegado y proporcionan un mecanismo para el acoplamiento débil entre objetos. Un evento permite a una clase exponer notificaciones sin exponer el delegado subyacente directamente.
Los eventos se usan a través de los siguientes pasos:
- Definir un delegado: Esto define la firma del método del controlador de eventos.
- Declarar un evento: Usando la palabra clave
event
, basado en el delegado definido. - Lanzar el evento: Cuando ocurre el evento, la clase lanza el evento, lo que invoca a los controladores de eventos registrados.
- Suscribirse/Darse de baja del evento: Otras clases u objetos pueden suscribirse al evento para ser notificados cuando se lanza usando
+=
y darse de baja usando-=
.
Por ejemplo:
public delegate void MyEventHandler(object sender, EventArgs e); public class MyClass { public event MyEventHandler MyEvent; protected virtual void OnMyEvent(EventArgs e) { MyEvent?.Invoke(this, e); } public void DoSomething() { // ... algo de código OnMyEvent(EventArgs.Empty); // Lanzar el evento } }
10. Describa el propósito del manejo de excepciones en C#.
El manejo de excepciones en C# es un mecanismo para tratar errores en tiempo de ejecución, asegurando que un programa no se bloquee inesperadamente y pueda recuperarse con gracia. Involucra monitorear un bloque de código en busca de posibles errores, capturar esos errores cuando ocurren, y luego ejecutar código específico para manejarlos, como registrar el error o intentar reintentar.
El propósito principal del manejo de excepciones gira en torno a estos elementos clave:
- Robustez: Evitar la terminación de la aplicación debido a problemas inesperados.
- Informes de errores: Proporcionar información significativa sobre los errores para la depuración y el registro.
- Gestión de recursos: Asegurar que los recursos se liberen correctamente, incluso en escenarios de error (por ejemplo, usando bloques
finally
). - Claridad del código: Separar la lógica de manejo de errores de la ejecución normal del programa, mejorando la legibilidad.
11. ¿Qué es LINQ y cuáles son sus beneficios?
LINQ (Language Integrated Query, Consulta Integrada en el Lenguaje) es un conjunto de características introducidas en .NET que proporciona una forma unificada de consultar datos de diversas fuentes. Estas fuentes pueden incluir bases de datos, documentos XML, colecciones y más. Permite usar una sintaxis consistente para realizar operaciones como filtrar, ordenar, agrupar y unir datos, independientemente de la fuente de datos subyacente.
Los beneficios de LINQ son numerosos:
- Legibilidad y Mantenibilidad: Las consultas LINQ son a menudo más concisas y fáciles de entender que las consultas SQL tradicionales o la manipulación de datos basada en código.
- Seguridad de Tipos: LINQ proporciona verificación de tipos en tiempo de compilación, lo que reduce los errores en tiempo de ejecución.
- Soporte de IntelliSense: Visual Studio proporciona soporte de IntelliSense para consultas LINQ, lo que facilita la escritura de código correcto.
- Agnóstico a la Fuente de Datos: La misma sintaxis LINQ se puede utilizar para consultar diferentes fuentes de datos.
- Productividad: LINQ simplifica el acceso y la manipulación de datos, lo que conduce a una mayor productividad del desarrollador. Ejemplo:
var resultado = from x in lista where x > 5 select x;
12. ¿Qué es un método de extensión y cómo se crea uno?
Un método de extensión permite agregar nuevos métodos a los tipos existentes sin modificar la definición del tipo original. Esto es útil cuando se desea agregar funcionalidad a un tipo que no se controla (por ejemplo, una clase de una biblioteca de terceros o un tipo incorporado como string
).
Para crear un método de extensión en C#, se define un método estático en una clase estática. El primer parámetro del método especifica el tipo que el método extiende, y está precedido por la palabra clave this
. Por ejemplo:
public static class StringExtensions { public static string ToTitleCase(this string str) { // Implementación para convertir la cadena a mayúsculas y minúsculas return System.Globalization.CultureInfo.CurrentCulture.TextInfo.ToTitleCase(str.ToLower()); } }
Ahora puedes llamar a ToTitleCase()
como si fuera un método de instancia de la clase string
:
string miCadena = "hola mundo"; string cadenaEnMayusculas = miCadena.ToTitleCase(); // cadenaEnMayusculas será "Hola Mundo"
13. Explica la diferencia entre las palabras clave 'const' y 'readonly'.
Tanto const
como readonly
se utilizan para evitar la modificación de una variable después de su asignación inicial, pero operan a diferentes niveles.
const
significa que el valor de la variable se conoce en tiempo de compilación y no se puede cambiar en tiempo de ejecución. Es una constante en tiempo de compilación. Por otro lado, readonly
significa que la variable solo puede recibir un valor durante la declaración o dentro del constructor de la clase/estructura a la que pertenece. Su valor no es necesariamente conocido en tiempo de compilación y puede ser diferente para diferentes instancias de la clase. Considera estos ejemplos:
public class Ejemplo { public readonly int valorEnTiempoDeEjecución; public const int valorEnTiempoDeCompilación = 5; public Ejemplo(int valor) { this.valorEnTiempoDeEjecución = valor; // Permitido //valorEnTiempoDeCompilación = 10; // No permitido } }
14. ¿Qué son los genéricos en C# y por qué son útiles?
Los genéricos en C# te permiten definir estructuras de datos y algoritmos con seguridad de tipos sin comprometerte con un tipo de datos específico. Usas parámetros de tipo (por ejemplo, T
) como marcadores de posición, que luego se reemplazan con tipos concretos cuando se utiliza el código. Evitan la sobrecarga de boxing/unboxing y permiten la verificación de tipos en tiempo de compilación, previniendo errores en tiempo de ejecución.
Los genéricos son útiles porque promueven la reutilización del código, la seguridad de tipos y el rendimiento. Sin genéricos, podrías usar el tipo object
, lo que requeriría casting e introduciría potencialmente errores en tiempo de ejecución. Con los genéricos, puedes escribir código que funciona con diferentes tipos de datos de forma segura y mejora la claridad del código. Por ejemplo, List<int>
es una lista de enteros, y List<string>
es una lista de cadenas, ambos usan la misma definición List<T>
. Esto elimina el código redundante.
15. Describe el propósito de los atributos en C#.
Los atributos en C# proporcionan una forma de agregar información declarativa a los elementos del código (ensamblados, módulos, tipos, miembros, parámetros, etc.). Se utilizan para almacenar metadatos sobre estos elementos, a los que luego se puede acceder en tiempo de ejecución utilizando la reflexión. Estos metadatos se pueden utilizar para diversos fines, tales como:
- Serialización: Especificar cómo se deben serializar los objetos. Por ejemplo, el atributo
[Serializable]
marca una clase como serializable. - Análisis de código: Proporcionar información para herramientas de análisis estático.
- Directivas del compilador: Influir en el comportamiento del compilador. Por ejemplo, el atributo
[Obsolete]
marca el código como obsoleto. - Comportamiento en tiempo de ejecución: Modificar el comportamiento del código en tiempo de ejecución. Por ejemplo, los atributos se utilizan ampliamente en ASP.NET Core para el enrutamiento y la validación.
Los atributos mejoran la legibilidad y el mantenimiento del código al separar los metadatos de la lógica principal. Reducen la necesidad de archivos de configuración complejos o la gestión de metadatos basada en código.
16. ¿Qué es boxing y unboxing en C#?
Boxing es el proceso de convertir un tipo de valor (como int
, bool
, struct
) a un tipo de referencia (object
). Implícitamente crea un objeto en el heap y copia los datos del tipo de valor en él. Unboxing es el proceso inverso; extrae explícitamente el tipo de valor del objeto.
Por ejemplo:
int i = 123; object box = i; // Boxing int j = (int)box; // Unboxing
El boxing implica asignación y copia de memoria, por lo que un boxing/unboxing excesivo puede afectar negativamente al rendimiento. El unboxing también requiere un cast explícito, y se lanza una InvalidCastException
si el objeto no contiene el tipo de valor correcto.
17. Explica la diferencia entre los operadores 'as' e 'is'.
Los operadores is
y as
en C# (y lenguajes similares) sirven para propósitos diferentes relacionados con la verificación de tipos y la conversión de tipos.
is
: Este operador comprueba si un objeto es compatible con un tipo dado. Devuelve un valor booleano (true
si el objeto es de ese tipo o puede ser convertido implícitamente a ese tipo, yfalse
en caso contrario). No realiza ninguna conversión de tipo. Por ejemplo:if (obj is string) { // hacer algo }
as
: Este operador intenta convertir un objeto a un tipo especificado. Si la conversión tiene éxito, devuelve el objeto como ese tipo. Si la conversión no es posible (por ejemplo, el objeto no es de ese tipo o no se puede convertir), devuelvenull
. Por ejemplo:string str = obj as string; if (str != null) { // usar str }
18. ¿Cuál es el propósito de la palabra clave 'sealed'?
La palabra clave sealed
en lenguajes como C# y Java (menos directamente) se utiliza para restringir la herencia. Cuando se aplica a una clase, evita que otras clases hereden de ella, asegurando que la implementación de la clase permanezca final y no se pueda modificar o extender a través de la herencia.
Esto es útil para:
- Seguridad: Evitar que clases derivadas maliciosas alteren el comportamiento.
- Rendimiento: El compilador puede optimizar mejor las clases selladas porque conoce el tipo exacto en tiempo de compilación.
- Control de versiones: Los cambios en una clase sellada no romperán accidentalmente clases derivadas en otros ensamblados.
- Control de diseño: Prevenir la extensión inesperada.
19. Describe la diferencia entre la memoria de pila y la de montón.
La memoria de pila se utiliza para la asignación de memoria estática, como variables locales y datos de llamadas a funciones. Es administrada automáticamente por el sistema utilizando una estructura LIFO (Last-In, First-Out). Es más rápido de acceder, pero tiene un tamaño limitado.
La memoria de montón, por otro lado, se utiliza para la asignación dinámica de memoria. La memoria se asigna y desasigna explícitamente por el programador utilizando funciones como malloc()
/free()
en C o new
/delete
en C++. El acceso al montón es más lento, pero proporciona más flexibilidad y un grupo de memoria más grande. Pueden ocurrir fugas de memoria si la memoria del montón no se desasigna correctamente.
20. ¿Qué son los tipos que aceptan valores NULL en C# y por qué son útiles?
Los tipos que aceptan valores NULL en C# te permiten asignar null
a tipos de valor (como int
, bool
, DateTime
, etc.). Los tipos de valor, por defecto, no aceptan valores NULL, lo que significa que siempre deben tener un valor. Los tipos que aceptan valores NULL se declaran usando el símbolo ?
después del tipo, por ejemplo, int?
, bool?
.
Son útiles cuando necesitas representar la ausencia de un valor para un tipo de valor. Por ejemplo:
- Cuando se trata de campos de base de datos que pueden ser
NULL
. - Representar datos opcionales en objetos de transferencia de datos (DTOs).
- Indicar que un valor no se ha inicializado o es desconocido.
int? nullableInt = null; if (nullableInt.HasValue) { int value = nullableInt.Value; Console.WriteLine(value); } else { Console.WriteLine("nullableInt es null"); }
21. Explica cómo funciona la recolección de basura en C#.
La recolección de basura (GC) en C# es una característica de administración automática de memoria. El CLR (Common Language Runtime) de .NET recupera automáticamente la memoria que ya no está en uso por la aplicación, evitando fugas de memoria. El GC opera en un sistema generacional.
Cuando se crea un objeto, se coloca en la Generación 0. El recolector de basura (GC) comprueba la memoria periódicamente. Si un objeto en la Generación 0 todavía está en uso, se promueve a la Generación 1. Con menos frecuencia, se comprueban los objetos de la Generación 1; los supervivientes se mueven a la Generación 2. Los objetos en la Generación 2 han sobrevivido a múltiples ciclos de recolección de basura. Este enfoque optimiza el rendimiento porque los objetos utilizados con frecuencia se comprueban con menos frecuencia. Cuando la memoria es baja, el GC se ejecuta, liberando memoria mediante la recolección de objetos a los que ya no se hace referencia. El método Dispose()
(y la instrucción using
) permite la limpieza determinista para los objetos que contienen recursos no administrados, complementando la gestión automática de la memoria administrada del GC.
22. ¿Qué es la programación asíncrona en C# y cuándo debería usarla?
La programación asíncrona en C# permite que su programa inicie una operación de larga duración sin bloquear el hilo principal. Esto significa que la interfaz de usuario permanece receptiva y la aplicación no se congela mientras espera a que se completen tareas como solicitudes de red, E/S de archivos o consultas a bases de datos. C# logra esto principalmente utilizando las palabras clave async
y await
.
Debería usar la programación asíncrona cuando se enfrenta a operaciones que pueden tomar una cantidad significativa de tiempo, particularmente cuando estas operaciones podrían bloquear el hilo de la interfaz de usuario. Los escenarios comunes incluyen:
- Aplicaciones de interfaz de usuario: Para mantener la interfaz de usuario receptiva durante operaciones largas.
- Aplicaciones web: Para manejar múltiples peticiones concurrentemente sin bloquear hilos.
- Operaciones vinculadas a E/S: Tareas que involucran leer o escribir archivos, flujos de red o bases de datos.
- Operaciones vinculadas a la CPU: Aunque menos común, puede mejorar la capacidad de respuesta si se manejan en un hilo separado (considere
Task.Run
conasync
yawait
).
23. Describa el propósito de las palabras clave 'virtual' y 'override'.
La palabra clave virtual
permite que un método en una clase base se anule en una clase derivada. Declara que las clases derivadas pueden proporcionar su propia implementación específica del método. Sin virtual
, siempre se utiliza la implementación de la clase base.
La palabra clave override
se utiliza en una clase derivada para proporcionar una nueva implementación para un método virtual
heredado de una clase base. Declara explícitamente que el método de la clase derivada está reemplazando la funcionalidad del método de la clase base. El uso de override
fuerza la comprobación en tiempo de compilación para asegurar que la firma del método coincida con el método virtual de la clase base. También indica la intención, haciendo que el código sea más claro y fácil de mantener.
24. ¿Cuál es la diferencia entre una estructura (struct) y una clase?
La principal diferencia entre una estructura (struct) y una clase reside en sus modificadores de acceso predeterminados. En C++, las estructuras (structs) tienen miembros que son públicos por defecto, mientras que los miembros de clase son privados por defecto. Esto significa que si no especifica explícitamente un modificador de acceso (public, private, protected) para un miembro en una estructura (struct), se podrá acceder a él desde cualquier lugar. Por el contrario, en una clase, solo se puede acceder a los miembros sin un modificador de acceso explícito desde dentro de la propia clase o de sus amigos.
En la práctica, las estructuras (structs) se utilizan a menudo para representar estructuras de datos simples donde se pretende acceder directamente a los miembros, mientras que las clases se utilizan para objetos más complejos con datos y métodos encapsulados. Sin embargo, funcionalmente, tanto las estructuras (structs) como las clases pueden tener métodos, constructores, destructores y herencia, lo que las hace muy similares en capacidad. La elección entre usar una estructura (struct) y una clase a menudo depende del uso previsto y del nivel de encapsulación deseado. Por ejemplo:
struct Point { int x; // público por defecto int y; }; class Rectangle { int width; // privado por defecto int height; public: void setWidth(int w) { width = w; } void setHeight(int h) { height = h; } };
25. Explique el concepto de expresiones lambda en C#.
Las expresiones lambda en C# son funciones anónimas que pueden tratarse como valores. Proporcionan una forma concisa de crear delegados de funciones o tipos de árbol de expresiones.
Normalmente se utilizan para definiciones de funciones cortas e inline, especialmente en consultas LINQ y controladores de eventos. La sintaxis básica es (parámetros-de-entrada) => expresión
. Por ejemplo, x => x * x
es una expresión lambda que toma una entrada x
y devuelve su cuadrado. Múltiples parámetros están separados por comas, (x, y) => x + y
. Las expresiones lambda también pueden tener cuerpos de instrucción encerrados entre llaves, (x) => { return x * 2; }
.
26. ¿Qué son los inicializadores de colección en C#?
Los inicializadores de colección proporcionan una sintaxis simplificada para inicializar colecciones (como listas, diccionarios, etc.) cuando se crean. En lugar de agregar elementos uno por uno usando el método Add()
, puedes especificar los elementos iniciales directamente dentro de llaves {}
.
Por ejemplo, considera una lista de enteros: List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
. O un diccionario: Dictionary<string, int> ages = new Dictionary<string, int> { { "Alice", 30 }, { "Bob", 25 } };
. Esto hace que el código sea más conciso y legible.
Preguntas de entrevista intermedias de C#
1. Explica la diferencia entre Func<T, TResult>
y Action<T>
en C#.
Func<T, TResult>
y Action<T>
son ambos delegados en C#, pero sirven para diferentes propósitos. Func<T, TResult>
representa un método que toma uno o más parámetros de entrada de tipo T
y devuelve un valor de tipo TResult
. El último parámetro de tipo genérico siempre denota el tipo de retorno. Por ejemplo, Func<int, string>
representa una función que toma un entero y devuelve una cadena.
Action<T>
por otro lado, representa un método que toma uno o más parámetros de entrada de tipo T
pero no devuelve un valor (es decir, su tipo de retorno es void
). Por ejemplo, Action<string>
representa un método que toma una cadena como entrada y realiza alguna acción, pero no devuelve nada. En esencia, Func
es para funciones que calculan y devuelven un resultado, mientras que Action
es para procedimientos que realizan una operación sin devolver un valor.
2. ¿Qué son los métodos de extensión y cómo se pueden usar? Proporcione un ejemplo.
Los métodos de extensión le permiten agregar nuevos métodos a tipos existentes sin modificar el tipo original en sí ni crear un nuevo tipo derivado. Son métodos estáticos especiales definidos en una clase estática, donde el primer parámetro especifica el tipo que extiende el método, precedido por la palabra clave this
. Esto le permite llamar al método de extensión como si fuera un miembro del tipo extendido.
Por ejemplo, para agregar un método que cuente las palabras en una cadena:
public static class StringExtensions { public static int WordCount(this string str) { return str.Split(new char[] { ' ', '.', '?' }, StringSplitOptions.RemoveEmptyEntries).Length; } } //Uso string text = "¡Hola mundo!"; int count = text.WordCount(); // count será 2
3. ¿Cómo funciona la palabra clave `yield` en C#, y qué problema resuelve?
La palabra clave yield
en C# permite crear un método iterador que devuelve una secuencia de valores de uno en uno, sin tener que crear una colección temporal para almacenar todo el resultado. Cuando se ejecuta la instrucción yield return
, el valor actual se devuelve al que llama, y se guarda el estado del método. La ejecución se reanuda desde ese punto la próxima vez que se llama al iterador. yield break
se puede utilizar para finalizar la iteración.
El problema que yield
resuelve está relacionado principalmente con la eficiencia de la memoria y la ejecución diferida. En lugar de generar una lista completa en la memoria antes de devolverla, yield
permite producir valores solo cuando se solicitan. Esto es particularmente útil cuando se trata de grandes conjuntos de datos o secuencias infinitas, ya que solo se procesan los elementos según sea necesario, mejorando así el rendimiento y reduciendo el uso de memoria.
4. ¿Cuál es el propósito de las palabras clave async
y await
en C#, y cómo funcionan juntas?
Las palabras clave async
y await
en C# se utilizan para escribir código asíncrono de forma más limpia y legible. async
marca un método como asíncrono, permitiendo el uso de await
dentro de él. La palabra clave await
pausa la ejecución del método async
hasta que la tarea esperada se completa, sin bloquear el hilo que llama. La ejecución se reanuda en el punto posterior al await
una vez que la tarea finaliza.
Trabajan juntos para que las operaciones asíncronas parezcan más código síncrono. El compilador transforma el método async
en una máquina de estados, manejando las devoluciones de llamada y continuaciones tras bambalinas. Esto evita el enfoque basado en devoluciones de llamada, complejo y propenso a errores, de la programación asíncrona. El método async
debe devolver Task
, Task<T>
, o void
. El uso de async
y await
mejora significativamente la capacidad de respuesta de las aplicaciones, especialmente aquellas que involucran operaciones ligadas a E/S.
5. Describe la diferencia entre `Task.Run()` y `Task.Factory.StartNew()`.
Task.Run()
es un contenedor simplificado alrededor de Task.Factory.StartNew()
. Task.Run()
está diseñado para el escenario más común: poner en cola trabajo en el grupo de subprocesos. Infiere automáticamente las opciones de creación de tareas apropiadas. En el fondo, Task.Run(action)
es equivalente a Task.Factory.StartNew(action, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default)
.
Task.Factory.StartNew()
ofrece más control y flexibilidad. Permite especificar parámetros como CancellationToken
, TaskCreationOptions
, y un TaskScheduler
personalizado. Esto es útil para escenarios avanzados, como especificar el comportamiento de la tarea (por ejemplo, LongRunning
, AttachedToParent
) o ejecutar la tarea en un programador específico distinto del programador predeterminado del grupo de subprocesos. Task.Run()
generalmente se prefiere para tareas simples de "fire-and-forget" debido a su simplicidad, mientras que Task.Factory.StartNew()
se usa cuando se requiere un control más detallado.
6. ¿Qué es LINQ y cómo mejora la legibilidad y el mantenimiento del código? Proporcione un ejemplo básico.
LINQ (Language Integrated Query) es un conjunto de características en .NET que proporciona una forma unificada de consultar datos de diversas fuentes de datos. Estas fuentes pueden incluir colecciones (como listas y arrays), bases de datos, documentos XML y más. LINQ le permite usar una sintaxis consistente para filtrar, ordenar, agrupar y proyectar datos, independientemente de la fuente de datos subyacente. Esto conduce a un código más legible y mantenible porque los mismos patrones de consulta se pueden aplicar a diferentes tipos de datos. Simplifica el acceso a los datos al incrustar consultas directamente en el código C# o VB.NET, eliminando la necesidad de lenguajes de consulta o API separadas.
Por ejemplo, considere una lista de enteros. Sin LINQ, filtrar los números pares podría implicar un bucle y sentencias condicionales. Con LINQ, es mucho más limpio:
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 }; //Usando LINQ IEnumerable<int> evenNumbers = numbers.Where(n => n % 2 == 0); //Sin LINQ List<int> evenNumbersList = new List<int>(); foreach (int number in numbers) { if (number % 2 == 0) { evenNumbersList.Add(number); } }
LINQ mejora la legibilidad porque la intención de la consulta (filtrar los números pares) es inmediatamente clara. Mejora el mantenimiento al reducir la cantidad de código repetitivo requerido para la manipulación de datos y usar operaciones comunes, lo que hace que el código sea más fácil de entender y modificar.
7. Explique el concepto de ejecución diferida en LINQ.
La ejecución diferida en LINQ significa que una consulta no se ejecuta en el punto donde se define. En cambio, la ejecución se pospone hasta que los resultados de la consulta son realmente necesarios. Esto ocurre típicamente cuando itera sobre los resultados usando un bucle foreach
, convierte los resultados en una lista (por ejemplo, usando .ToList()
), o llama a un método que requiere evaluación inmediata (por ejemplo, .Count()
, .First()
, .Single()
).
El principal beneficio de la ejecución diferida es la optimización del rendimiento. Los proveedores de LINQ pueden optimizar la ejecución de la consulta en función de cómo se utilizan finalmente los resultados. Si solo necesita los primeros elementos de un conjunto de datos grande, no es necesario procesar todo el conjunto de datos inmediatamente. Además, le permite modificar la fuente de datos subyacente después de definir la consulta pero antes de que se ejecute, y la consulta reflejará esos cambios.
8. Describa las diferencias entre `IEnumerable` y `IQueryable`.
IEnumerable
e IQueryable
son ambas interfaces en .NET que se utilizan para consultar datos, pero difieren significativamente en cómo se recuperan y procesan los datos. IEnumerable
representa una secuencia de objetos sobre los que se puede iterar. Cuando se utiliza IEnumerable
, toda la fuente de datos se carga en la memoria, y el filtrado, la clasificación y otras operaciones se realizan en el lado del cliente. Esto significa que las operaciones ocurren después de que se recuperan los datos.
IQueryable
, por otro lado, representa una consulta que se puede ejecutar contra una fuente de datos. Permite construir una expresión de consulta que luego se traduce y se ejecuta en la fuente de datos (por ejemplo, una base de datos). Esto significa que el filtrado, la clasificación y otras operaciones se realizan en el lado del servidor antes de que se recuperen los datos, lo que lleva a ganancias de rendimiento potencialmente significativas, especialmente cuando se trata de conjuntos de datos grandes. La clave es que IQueryable
usa árboles de expresión para representar la consulta, que pueden ser analizados y optimizados por la fuente de datos.
Diferencias clave:
- Ejecución:
IEnumerable
ejecuta consultas en memoria (lado del cliente), mientras queIQueryable
ejecuta consultas en la fuente de datos (lado del servidor). - Carga de datos:
IEnumerable
carga todo el conjunto de datos, mientras queIQueryable
solo recupera los datos que coinciden con la consulta. - Rendimiento:
IQueryable
es generalmente más eficiente para conjuntos de datos grandes debido al procesamiento del lado del servidor. - Espacio de nombres:
IEnumerable
reside enSystem.Collections
,IQueryable
reside enSystem.Linq
. IQueryable
hereda deIEnumerable
.
9. ¿Cuáles son los beneficios de usar interfaces en C#? Explique con un ejemplo.
Las interfaces en C# ofrecen varios beneficios, principalmente en torno a la abstracción y el desacoplamiento. Definen un contrato que las clases pueden implementar, asegurando que proporcionen una funcionalidad específica. Esto permite el polimorfismo, donde diferentes clases pueden ser tratadas uniformemente a través de la interfaz.
Considere este ejemplo:
interface ISpeak { void Speak(); } class Dog : ISpeak { public void Speak() { Console.WriteLine("Woof!"); } } class Cat : ISpeak { public void Speak() { Console.WriteLine("Meow!"); } } //Uso ISpeak animal1 = new Dog(); ISpeak animal2 = new Cat(); animal1.Speak(); // Output: Woof! animal2.Speak(); // Output: Meow!
Beneficios:
- Abstracción: Ocultar detalles de implementación complejos.
- Desacoplamiento: Las clases que implementan la interfaz son independientes.
- Polimorfismo: Tratar diferentes clases uniformemente.
- Herencia Múltiple: Una clase puede implementar múltiples interfaces, superando la falta de herencia múltiple de clases de C#.
10. Explique la diferencia entre `struct` y `class` en C#.
En C#, la principal diferencia entre struct
y class
reside en su naturaleza: struct
es un tipo de valor, mientras que class
es un tipo de referencia. Esto significa que cuando asignas un struct
a una nueva variable o lo pasas como argumento, se crea una copia del struct
. Con class
, solo se copia una referencia al objeto. Otra diferencia significativa es que los structs
son implícitamente sellados, lo que significa que no se puede heredar de ellos, mientras que las classes
sí pueden heredarse.
Además, los structs
tienen un constructor sin parámetros implícito que inicializa los campos a sus valores predeterminados, y no puedes definir tu propio constructor sin parámetros. Las classes
, por otro lado, no tienen esta limitación. Asimismo, los structs
se usan típicamente para estructuras de datos pequeñas, mientras que las classes
se usan para objetos más complejos con comportamiento. Las variables struct
se asignan en la pila (stack), mientras que los objetos class
se asignan en el montón (heap).
11. ¿Cuándo elegirías usar un struct
en lugar de una class
en C#?
En C#, usa un struct
en lugar de una class
cuando necesitas una estructura de datos ligera que represente un único valor, y se desean semánticas de tipo valor. Los struct
son tipos valor, por lo que se copian al asignarlos, lo cual es beneficioso cuando quieres copias independientes de tus datos y evitar efectos secundarios no deseados. Elige struct
para tipos pequeños e inmutables como Point
, Rectangle
o contenedores de datos simples.
Por el contrario, prefiere class
cuando tratas con objetos más complejos que tienen un comportamiento significativo, requieren herencia, o se benefician de semánticas de tipo referencia (compartiendo la misma instancia en memoria). Las clases son tipos referencia y se asignan en el montón (heap), por lo tanto, son adecuados para objetos más grandes o situaciones donde la identidad del objeto importa.
12. ¿Qué es el boxing y unboxing en C#, y cuáles son las implicaciones de rendimiento?
Boxing es el proceso de convertir un tipo valor (como int
, bool
, struct
) a un tipo referencia (object
). Unboxing es el proceso inverso, convirtiendo un tipo referencia (que previamente fue boxed) de vuelta a su tipo valor original.
La principal implicación en el rendimiento se debe a la asignación de memoria y la comprobación de tipos involucradas. El boxing requiere asignar memoria en el heap para almacenar el tipo de valor, y el unboxing requiere la comprobación de tipos para asegurar que el objeto que se está desempaquetando es compatible con el tipo de valor de destino. Estas operaciones pueden ser relativamente lentas en comparación con las operaciones directas en los tipos de valor, especialmente si ocurren con frecuencia en código crítico para el rendimiento. Esto suele conducir a un mayor overhead de la recolección de basura.
13. ¿Cómo funciona el recolector de basura en C#? ¿Cuál es la diferencia entre las generaciones?
El recolector de basura (GC) de C# gestiona automáticamente la memoria reclamando objetos que ya no están en uso. Funciona inspeccionando periódicamente el heap, identificando los objetos que son inalcanzables desde las raíces de la aplicación (variables estáticas, variables locales en la pila, registros de la CPU) y liberando la memoria que ocupan. El GC utiliza un algoritmo de marcado y barrido, mejorado con generaciones para optimizar el rendimiento. El GC asume que los objetos creados recientemente son propensos a ser de corta duración y que los objetos más antiguos son menos propensos a ser basura.
Las generaciones son una parte clave de la estrategia de optimización del GC. Los objetos se agrupan en generaciones según su antigüedad: Generación 0 (la más joven), Generación 1 y Generación 2 (la más antigua). Cuando el GC se ejecuta, primero intenta recolectar la Generación 0. Si eso no libera suficiente memoria, recolecta la Generación 1 y luego la Generación 2 si es necesario. Este enfoque evita recolectar todo el montón de memoria cada vez, lo que mejora significativamente el rendimiento porque recolectar las generaciones más jóvenes es más rápido, la suposición es que la generación más joven tiene la mayor cantidad de objetos muertos en comparación con la cantidad de objetos vivos.
14. Explique el propósito de la declaración using
en C# y cómo se relaciona con la interfaz IDisposable
.
La declaración using
en C# proporciona una forma conveniente de asegurar que un objeto que implementa la interfaz IDisposable
se descarte correctamente, incluso si se producen excepciones. Esencialmente, envuelve el uso del objeto en un bloque try...finally
, donde el método Dispose()
se llama en el bloque finally
.
Cuando un objeto implementa IDisposable
, típicamente contiene recursos no administrados como identificadores de archivos, conexiones de red o conexiones de bases de datos. El método Dispose()
libera estos recursos. La declaración using
garantiza que Dispose()
se llame cuando se sale del bloque, lo que evita fugas de recursos. Por ejemplo:
using (FileStream fs = new FileStream("archivo.txt", FileMode.Open)) { // Usa el flujo de archivo } // fs.Dispose() se llama automáticamente aquí
15. ¿Qué son los delegados en C# y cómo se utilizan para implementar el manejo de eventos?
Los delegados en C# son punteros a funciones con seguridad de tipos. Permiten tratar los métodos como objetos, que se pueden pasar como argumentos a otros métodos, almacenar en estructuras de datos e invocar dinámicamente. Esencialmente, definen un tipo que representa una referencia a un método con una firma específica (tipo de retorno y tipos de parámetros).
Los delegados son cruciales para implementar el manejo de eventos en C#. Los eventos son un mecanismo para que una clase notifique a otras clases (u objetos) cuando ocurre algo de interés. Esto se logra utilizando delegados. Un evento es esencialmente un envoltorio alrededor de un delegado. Cuando ocurre un evento, se ejecuta la lista de invocación del delegado (una lista de métodos que deben llamarse cuando se activa el evento). Ejemplo:
public delegate void MyEventHandler(object sender, EventArgs e); public event MyEventHandler MyEvent;
16. Explique la diferencia entre delegados y eventos en C#.
Los delegados son punteros a funciones con seguridad de tipos, lo que permite tratar los métodos como objetos. Definen una firma y pueden contener referencias a métodos que coinciden con esa firma. Puede invocar directamente un delegado.
Los eventos, por otro lado, son un mecanismo construido sobre delegados que proporciona encapsulación. Evitan la invocación directa desde fuera de la clase o estructura que los define. Los eventos usan delegados internamente, pero agregan restricciones que garantizan que solo la clase que declara el evento pueda activarlo (invocarlo). Los suscriptores solo pueden agregar o eliminar controladores de eventos. Los eventos hacen cumplir un patrón de publicación-suscripción.
17. Describa cómo implementaría una excepción personalizada en C#.
Para implementar una excepción personalizada en C#, se crea una nueva clase que hereda de la clase System.Exception
o de una de sus clases derivadas (como System.ApplicationException
o System.SystemException
). Es una buena práctica proporcionar constructores similares a los que se encuentran en System.Exception
: un constructor predeterminado, un constructor que acepta una cadena de mensaje y un constructor que acepta una cadena de mensaje y una excepción interna.
Aquí hay un ejemplo básico:
public class MyCustomException : Exception { public MyCustomException() { } public MyCustomException(string message) : base(message) { } public MyCustomException(string message, Exception inner) : base(message, inner) { } }
Luego, puede lanzar esta excepción como cualquier otra excepción:
throw new MyCustomException("¡Algo salió mal!");
Considere agregar propiedades o métodos personalizados para contener o manejar datos específicos de la excepción, como un código de error, si es necesario.
18. ¿Qué son los atributos en C#, y cómo se pueden crear y usar?
Los atributos en C# son metadatos que proporcionan información sobre los tipos (clases, estructuras, enumeraciones, interfaces, delegados), métodos, campos, propiedades, eventos, parámetros y otros elementos de código. Se utilizan para agregar información declarativa a su código, que luego puede leerse y actuar sobre ella en tiempo de ejecución o en tiempo de compilación utilizando reflexión. Proporcionan una forma poderosa de agregar comportamiento o modificar el proceso de compilación sin alterar la lógica principal de su código.
Para crear un atributo personalizado, define una clase que hereda de System.Attribute
. Luego, aplica el atributo a los elementos de código utilizando corchetes []
seguidos del nombre del atributo y cualquier parámetro de constructor. Aquí hay un ejemplo:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class MyCustomAttribute : Attribute { public string Description { get; set; } public MyCustomAttribute(string description) { Description = description; } } [MyCustomAttribute("This is a class description")] public class MyClass { [MyCustomAttribute("This is a method description")] public void MyMethod() { } }
Para acceder a los atributos en tiempo de ejecución, usa reflexión:
Type type = typeof(MyClass); MyCustomAttribute attribute = (MyCustomAttribute)Attribute.GetCustomAttribute(type, typeof(MyCustomAttribute)); if (attribute != null) { Console.WriteLine(attribute.Description); }
19. Explique qué es la reflexión en C#, y proporcione un escenario donde podría ser útil.
La reflexión en C# te permite inspeccionar y manipular tipos (clases, interfaces, estructuras, enumeraciones, delegados y eventos) en tiempo de ejecución. En lugar de conocer el tipo en tiempo de compilación, puedes descubrir sus propiedades, métodos, campos, eventos y otros miembros durante la ejecución del programa. Esencialmente, es una forma de que el código examine y modifique su propia estructura y comportamiento.
Un escenario útil es crear un serializador/deserializador de objetos genéricos. Podría usar reflexión para iterar a través de las propiedades de un objeto arbitrario, extraer sus valores y luego usar esa información para serializar el objeto en un formato como JSON o XML. Por el contrario, durante la deserialización, podría usar reflexión para crear una instancia del objeto y rellenar sus propiedades con datos extraídos del formato serializado. Esto evita escribir código de serialización/deserialización específico para cada clase.
20. ¿Qué son los genéricos en C# y cómo mejoran la seguridad de tipos y el rendimiento?
Los genéricos en C# le permiten definir estructuras de datos y algoritmos seguros para tipos sin comprometerse con tipos de datos específicos. Puede definir clases, interfaces, métodos, delegados y eventos que están parametrizados por tipo. El parámetro de tipo se especifica cuando se crea una instancia del tipo genérico o se llama al método genérico.
Los genéricos mejoran la seguridad de tipos al garantizar que el compilador imponga restricciones de tipo en tiempo de compilación. Esto evita la necesidad de comprobación y conversión de tipos en tiempo de ejecución, lo que puede provocar errores InvalidCastException
. Los genéricos también mejoran el rendimiento al evitar operaciones de boxing y unboxing cuando se trabaja con tipos de valor. Esto se debe a que el compilador puede generar código especializado para cada tipo utilizado con el tipo o método genérico, lo que resulta en una ejecución más eficiente. El siguiente fragmento de código es un ejemplo de una lista genérica.
List<int> números = new List<int>(); números.Add(1); números.Add(2); List<string> nombres = new List<string>(); nombres.Add("Alice"); nombres.Add("Bob");
Preguntas avanzadas de entrevista de C#
1. Explique las diferencias entre `async` y `await` y cómo contribuyen a la creación de aplicaciones receptivas.
`async` y `await` son azúcar sintáctico construido sobre las Promesas en JavaScript, diseñado para que el código asíncrono sea más fácil de leer y escribir. `async` declara una función como asíncrona, lo que permite el uso de `await` dentro de ella. `await` pausa la ejecución de la función `async` hasta que la Promesa que 'espera' se resuelve.
Al pausar la ejecución en lugar de bloquear el hilo principal, `async/await` permite que el bucle de eventos continúe procesando otras tareas, como manejar las interacciones del usuario o renderizar actualizaciones. Este comportamiento no bloqueante es crucial para construir aplicaciones receptivas, evitando que la interfaz de usuario se congele durante operaciones de larga duración como peticiones de red o cálculos complejos. En lugar de `.then()` y `.catch()` para manejar las promesas, podemos escribir código con un estilo más síncrono. Por ejemplo:
async function fetchData() { try { const response = await fetch('https://example.com/data'); const data = await response.json(); return data; } catch (error) { console.error('Error al obtener datos:', error); } }
2. ¿Cómo funciona el recolector de basura de .NET y cuáles son las diferentes generaciones de recolección de basura?
El recolector de basura (.NET GC) gestiona automáticamente la asignación y liberación de memoria para las aplicaciones. Reclama la memoria ocupada por objetos que ya no están en uso. El GC opera basándose en el principio de la recolección de basura generacional para mejorar el rendimiento. Cuando el GC se ejecuta, determina qué objetos ya no están siendo utilizados por la aplicación. Luego, reclama la memoria ocupada por esos objetos.
.NET GC utiliza generaciones para categorizar objetos en función de su edad:
- Generación 0: Objetos de corta duración, como variables temporales. Esta generación se recolecta con frecuencia.
- Generación 1: Objetos que han sobrevivido a una recolección de la Generación 0. Sirve como un búfer entre los objetos de corta y larga duración.
- Generación 2: Objetos de larga duración que han sobrevivido a múltiples ciclos de recolección de basura. Esta generación se recolecta con menos frecuencia porque contiene objetos que probablemente permanecerán en memoria durante la duración de la vida útil de la aplicación. Una recolección de basura completa involucra a todas las generaciones (0, 1 y 2).
3. Describe el propósito de la interfaz IDisposable
y la declaración using
en C#, y explica cómo ayudan a administrar los recursos.
La interfaz IDisposable
en C# proporciona un mecanismo para liberar recursos no gestionados (por ejemplo, identificadores de archivos, conexiones de red, conexiones de bases de datos) mantenidos por un objeto. Define un solo método, Dispose()
, que debe contener la lógica para limpiar estos recursos. Las clases que implementan IDisposable
señalan que contienen recursos que deben ser liberados explícitamente.
La declaración using
simplifica el proceso de llamar a Dispose()
al asegurar que siempre se llama, incluso si ocurren excepciones dentro del bloque using
. Cuando se crea un objeto dentro de una declaración using
, el método Dispose()
se llama automáticamente cuando se sale del bloque. Esto promueve la gestión determinista de recursos, previniendo fugas de recursos y mejorando la estabilidad de la aplicación. La sintaxis de una declaración using
es using (ResourceType resource = new ResourceType()) { ... }
4. ¿Qué son los delegados y eventos en C#, y cómo facilitan la comunicación entre objetos?
Los delegados son punteros de función con seguridad de tipos. Contienen la referencia a un método, lo que le permite pasar métodos como argumentos a otros métodos. Los eventos, por otro lado, son un mecanismo para que una clase (publicador) notifique a otras clases (suscriptores) cuando ocurre algo de interés. Se basan en delegados.
Los delegados y eventos facilitan la comunicación entre objetos al permitir el acoplamiento débil. El publicador no necesita conocer los suscriptores específicos; solo necesita generar el evento. Los suscriptores registran su interés en el evento a través de los controladores de delegados. Cuando se genera el evento, se llama a la lista de invocación del delegado, lo que activa los métodos asociados en los objetos suscriptores. Este patrón de publicación-suscripción permite una forma flexible y extensible para que los objetos interactúen sin dependencias estrechas.
5. Explique el concepto de LINQ (Language Integrated Query) y cómo simplifica la consulta de datos en C#.
LINQ (Language Integrated Query) es una característica poderosa en C# que proporciona una forma unificada de consultar datos de varias fuentes, como bases de datos, XML, colecciones y más, directamente dentro del código C#. Simplifica la consulta de datos al usar una sintaxis consistente, independientemente de la fuente de datos. En lugar de escribir código diferente para cada fuente de datos, LINQ permite a los desarrolladores usar un conjunto estándar de operadores de consulta (por ejemplo, Where
, Select
, OrderBy
) y sintaxis para filtrar, proyectar y ordenar datos.
LINQ ofrece varias ventajas, incluyendo una mejor legibilidad y mantenibilidad del código, una menor complejidad del código y comprobación de tipos en tiempo de compilación. Las consultas LINQ se pueden escribir utilizando sintaxis de consulta (similar a SQL) o sintaxis de método (usando expresiones lambda). Por ejemplo:
// Sintaxis de consulta var resultados = from item in collection where item.Property > 10 select item.Name; // Sintaxis de método var resultados = collection.Where(item => item.Property > 10).Select(item => item.Name);
6. ¿Qué son los métodos de extensión y cómo se pueden usar para agregar funcionalidad a las clases existentes sin modificar su código fuente?
Los métodos de extensión le permiten agregar nuevos métodos a los tipos existentes (clases, estructuras, interfaces) sin modificar el código fuente del tipo original. Son un tipo especial de método estático que se puede llamar como si fueran métodos de instancia del tipo extendido. Para definir un método de extensión en C#, necesita:
-
Declarar una clase
static
. -
Definir un método
static
dentro de esa clase. -
Usar la palabra clave
this
como el primer parámetro del método, seguido del tipo que desea extender. Por ejemplo:
public static class StringExtensions { public static bool IsValidEmail(this string str) { // Lógica de validación de correo electrónico aquí return str.Contains("@") && str.Contains("."); } }
Después de definir el método de extensión, puedes llamarlo en cualquier instancia del tipo extendido como si fuera un método de instancia regular: string email = "test@example.com"; bool isValid = email.IsValidEmail();
7. Describe las diferencias entre los tipos de valor y los tipos de referencia en C#, y cómo afectan a la gestión de memoria.
Los tipos de valor (por ejemplo, int
, bool
, struct
, enum
) almacenan sus datos directamente dentro de su asignación de memoria. Cuando asignas un tipo de valor a una nueva variable, se crea una copia de los datos. Cada variable tiene entonces su propia copia independiente. Los tipos de referencia (por ejemplo, string
, class
, array
, delegate
) almacenan una referencia (una dirección de memoria) a los datos reales almacenados en otro lugar del montón (heap). Cuando asignas un tipo de referencia a una nueva variable, solo se copia la referencia, no los datos subyacentes. Ambas variables apuntan entonces a la misma ubicación de memoria.
Esta diferencia impacta significativamente en la gestión de memoria. Los tipos de valor se asignan típicamente en la pila (stack), lo que proporciona una asignación y desasignación rápidas, y se desasignan automáticamente cuando salen del ámbito. Los tipos de referencia se asignan en el montón (heap), lo que requiere más sobrecarga para la asignación y la desasignación. La recolección de basura (garbage collection) es responsable de reclamar la memoria ocupada por los tipos de referencia que ya no están en uso, lo que introduce un costo de rendimiento.
8. ¿Qué es la reflexión en C#, y cómo se puede usar para inspeccionar y manipular tipos en tiempo de ejecución?
La reflexión en C# te permite inspeccionar y manipular tipos (clases, interfaces, estructuras, etc.) en tiempo de ejecución. Esto significa que puedes descubrir información sobre tipos, crear instancias de ellos, invocar sus métodos y acceder a sus propiedades, todo sin conocer sus nombres o estructuras en tiempo de compilación. Se logra principalmente a través de clases que se encuentran en el espacio de nombres System.Reflection
.
La reflexión se puede usar para varios propósitos:
- Inspeccionando Tipos: Descubrir información como métodos, propiedades, campos y atributos de un tipo utilizando métodos como
GetType()
,GetMethods()
,GetProperties()
, etc. - Creando Instancias: Creando instancias de tipos dinámicamente utilizando
Activator.CreateInstance()
. - Invocando Métodos: Llamando a métodos de un tipo dinámicamente utilizando
MethodInfo.Invoke()
. - Accediendo a Propiedades y Campos: Obteniendo y estableciendo valores de propiedades y campos utilizando
PropertyInfo.GetValue()
,PropertyInfo.SetValue()
,FieldInfo.GetValue()
yFieldInfo.SetValue()
.
Por ejemplo, el siguiente fragmento de código demuestra el uso de la reflexión:
Type myType = Type.GetType("MyNamespace.MyClass"); object instance = Activator.CreateInstance(myType); MethodInfo method = myType.GetMethod("MyMethod"); method.Invoke(instance, null);
9. Explique el propósito de los atributos en C# y cómo se pueden usar para agregar metadatos a elementos de código.
Los atributos en C# se utilizan para agregar metadatos declarativos a elementos de código (ensamblados, módulos, clases, métodos, propiedades, etc.). Proporcionan una forma de asociar información con el código a la que se puede acceder en tiempo de ejecución utilizando la reflexión. Estos metadatos se pueden utilizar para diversos fines, como la serialización, la validación, la generación de código o la documentación.
Los atributos se definen como clases que heredan de System.Attribute
. Se aplican a elementos de código colocándolos entre corchetes []
antes del elemento. Por ejemplo, [Obsolete("Use NewMethod instead")] public void OldMethod() { ... }
marca el OldMethod
como obsoleto, con el mensaje proporcionado. Luego, la reflexión se puede usar para leer este atributo Obsolete
y tomar la acción apropiada.
10. ¿Qué son los genéricos en C#, y cómo permiten la programación con seguridad de tipos con código reutilizable?
Los genéricos en C# permiten definir estructuras de datos y algoritmos con seguridad de tipos sin comprometerse con un tipo de datos específico. Usan parámetros de tipo (por ejemplo, List<T>
) que son marcadores de posición que se reemplazan con un tipo real cuando se usa el tipo genérico. Esto elimina la necesidad de conversión y reduce el riesgo de errores de tipo en tiempo de ejecución.
Los genéricos permiten la programación con seguridad de tipos porque el compilador aplica restricciones de tipo en tiempo de compilación. El código reutilizable se logra porque se puede escribir una sola clase o método genérico que funcione con múltiples tipos de datos sin duplicación de código. Por ejemplo, una clase de lista genérica puede almacenar enteros, cadenas o cualquier otro tipo de forma segura y eficiente, evitando la conversión de tipos asociada con las colecciones basadas en object
. Por ejemplo:
public class GenericList<T> { private T[] items; //... public T GetItem(int index) { return items[index]; } }
11. ¿Cómo funciona la palabra clave yield
en C#, y cómo se puede usar para crear iteradores?
La palabra clave yield
en C# se utiliza para crear iteradores de manera stateful (con estado). En lugar de devolver una colección a la vez, un método que usa yield
devuelve un IEnumerable
o IEnumerator
que produce elementos de uno en uno a medida que se solicitan. Cuando se encuentra yield return
, se devuelve el elemento actual y se conserva el estado del método. La ejecución se reanuda desde ese punto cuando se solicita el siguiente elemento.
Usando yield
, los iteradores personalizados se pueden crear fácilmente. Por ejemplo:
public IEnumerable<int> GetNumbers(int max) { for (int i = 0; i < max; i++) { yield return i; } }
Esto evita la necesidad de crear una clase separada que implemente IEnumerable
y IEnumerator
manualmente. yield break
se puede usar para indicar el final de la iteración.
12. Describa el concepto de covarianza y contravarianza en genéricos de C# y proporcione ejemplos de su uso.
La covarianza y la contravarianza describen cómo los parámetros de tipo en interfaces genéricas y delegados se pueden convertir implícitamente. La covarianza le permite usar un tipo más derivado que el especificado originalmente (por ejemplo, IEnumerable<string>
se puede usar donde se espera IEnumerable<object>
si string
hereda de object
). La contravarianza le permite usar un tipo menos derivado que el especificado originalmente (por ejemplo, una acción que acepta object
se puede usar donde se espera una acción que acepte string
).
La covarianza es compatible con los parámetros de tipo genéricos declarados como out
(salida), y la contravarianza es compatible con los parámetros de tipo declarados como in
(entrada). Por ejemplo:
- Covarianza:
interface ICovariant<out T> {}
ICovariant<string> cov = new Covariant<string>(); ICovariant<object> objCov = cov;
- Contravarianza:
interface IContravariant<in T> {}
IContravariant<object> contra = new Contravariant<object>(); IContravariant<string> strContra = contra;
Los delegados como Action<T>
y Func<T>
también admiten la varianza basada en sus tipos de parámetro. Action<object> objAction = (o) => {}; Action<string> stringAction = objAction;
(contravarianza). Func<string> stringFunc = () => "test"; Func<object> objectFunc = stringFunc;
(covarianza).
13. ¿Qué son las tuplas en C#, y cómo ofrecen una forma de agrupar múltiples valores en un solo objeto?
Las tuplas en C# son una forma de agrupar múltiples valores de tipos potencialmente diferentes en una única estructura de datos ligera. Antes de C# 7.0, esto se lograba a menudo con las clases System.Tuple
, pero eran engorrosas debido al nombre de los elementos (Item1
, Item2
, etc.). C# 7.0 introdujo las tuplas de valores, que son más eficientes y ofrecen campos con nombre.
Las tuplas de valores son estructuras, por lo que son tipos de valor (almacenados en la pila). Proporcionan una sintaxis concisa para crear y trabajar con datos compuestos. Por ejemplo, (int age, string name) person = (30, "Alice");
demuestra una tupla con campos con nombre. Luego, puede acceder a estos campos usando person.age
y person.name
. Las tuplas de valores mejoran la legibilidad del código y reducen la necesidad de definir clases o structs personalizados para agrupaciones de datos simples. Se crean fácilmente usando paréntesis y se pueden devolver como un único valor de retorno de una función.
14. Explique el propósito de la palabra clave `dynamic` en C#, y cómo habilita la programación de enlace tardío.
La palabra clave dynamic
en C# le permite evitar la verificación estática de tipos en tiempo de compilación. Las variables declaradas como dynamic
tienen su tipo determinado en tiempo de ejecución. Esto permite la programación de enlace tardío, donde las llamadas a métodos y el acceso a propiedades se resuelven durante la ejecución en lugar de durante la compilación.
Usar dynamic
es útil cuando se trabaja con objetos cuya estructura no se conoce en tiempo de compilación, como cuando se interactúa con objetos COM, lenguajes dinámicos como Python o Ruby, o cuando se usa reflexión. Sin embargo, es crucial tener en cuenta que los errores causados por el uso incorrecto de dynamic
solo se detectan en tiempo de ejecución, lo que puede llevar a un comportamiento inesperado. Aquí hay un ejemplo:
dynamic myVariable = GetUnknownObject(); myVariable.SomeMethod(); // Esto se compilará, pero podría lanzar una excepción en tiempo de ejecución si SomeMethod no existe
15. ¿Qué son las expresiones lambda en C#, y cómo se pueden usar para crear funciones anónimas?
Las expresiones lambda en C# son una forma concisa de crear funciones anónimas. Son esencialmente métodos sin nombre que pueden ser tratados como datos. La sintaxis es (parámetros de entrada) => expresión o bloque de sentencias
. Por ejemplo, x => x * x
es una expresión lambda que toma una entrada x
y devuelve su cuadrado.
Se utilizan para crear delegados o tipos de árboles de expresión. Puedes asignar una expresión lambda a un tipo delegado, como Func<int, int>
o Action<string>
. Son muy útiles en consultas LINQ, manejadores de eventos y cualquier escenario donde se necesita una función corta e integrada. Ejemplo: numbers.Where(n => n % 2 == 0)
filtra los números pares de una lista llamada numbers
.
16. Describe los diferentes tipos de colecciones disponibles en C#, como listas, diccionarios y conjuntos, y explica sus casos de uso.
C# ofrece varios tipos de colecciones, cada uno adecuado para diferentes escenarios. Las listas (List<T>
) son colecciones ordenadas que permiten elementos duplicados y proporcionan acceso por índice. Son ideales cuando necesitas mantener el orden de los elementos y acceder frecuentemente a los elementos por su posición. Los diccionarios (Dictionary<TKey, TValue>
) almacenan pares clave-valor, proporcionando búsquedas rápidas basadas en la clave. Úsalos cuando necesites recuperar valores rápidamente basados en un identificador único.
Conjuntos (HashSet<T>
) son colecciones desordenadas que almacenan elementos únicos. Son útiles para verificar la pertenencia de manera eficiente o eliminar entradas duplicadas. Otras colecciones incluyen Queue<T>
(FIFO), Stack<T>
(LIFO) y colecciones especializadas como ObservableCollection<T>
(para la vinculación de datos) y BlockingCollection<T>
(para operaciones seguras para subprocesos). Elegir la colección correcta depende de los requisitos específicos de su aplicación, considerando factores como el orden, la unicidad y los requisitos de rendimiento.
17. ¿Qué es la inyección de dependencias (DI) en C#, y cómo se puede usar para mejorar la capacidad de prueba y el mantenimiento del código?
La Inyección de Dependencias (DI) en C# es un patrón de diseño donde una clase recibe sus dependencias de fuentes externas en lugar de crearlas por sí misma. Esto promueve el acoplamiento suelto entre los componentes. En lugar de que una clase cree sus dependencias directamente (palabra clave new
), estas dependencias se "inyectan" en la clase, típicamente a través de su constructor, propiedades o métodos.
La DI mejora la capacidad de prueba porque las dependencias se pueden reemplazar fácilmente con objetos simulados o stubs durante las pruebas unitarias. Esto le permite aislar el código que se está probando y verificar su comportamiento sin depender de sistemas externos. El mantenimiento se mejora porque es menos probable que los cambios en una dependencia afecten a otras partes de la aplicación, debido al acoplamiento laxo. La DI también promueve la reutilización del código y facilita la refactorización del código.
18. Explique el concepto de la Biblioteca paralela de tareas (TPL) en C#, y cómo simplifica la programación paralela.
La Biblioteca paralela de tareas (TPL) en C# es un conjunto de clases y API en el espacio de nombres System.Threading.Tasks
que simplifica la adición de paralelismo y concurrencia a las aplicaciones. Abstrae muchas de las complejidades de trabajar directamente con hilos, lo que permite a los desarrolladores centrarse en el trabajo que se debe realizar en lugar de en la mecánica de la gestión de hilos. La TPL maneja automáticamente la partición del trabajo, la programación de hilos, la administración del grupo de hilos y la gestión de la cancelación, todo de forma eficiente y escalable. El componente principal de TPL es el objeto Task
que representa una operación asíncrona.
TPL simplifica la programación paralela al proporcionar una abstracción de nivel superior sobre los hilos. Los beneficios clave incluyen:
- Código simplificado: Los desarrolladores pueden expresar operaciones paralelas de manera más concisa usando las palabras clave
Task.Run
,Parallel.For
,Parallel.ForEach
yasync/await
. - Gestión automática de hilos: TPL se encarga de la gestión del grupo de hilos, reduciendo la sobrecarga de crear y destruir hilos manualmente.
- Gestión de excepciones: Proporciona una forma centralizada de manejar las excepciones lanzadas por las tareas.
- Soporte de cancelación: Soporte integrado para cancelar tareas.
- Paralelismo de datos: Simplifica el procesamiento paralelo de colecciones de datos usando
Parallel.For
yParallel.ForEach
.
19. ¿Qué son los flujos asíncronos en C# y cómo permiten el procesamiento de flujos de datos de forma asíncrona?
Los flujos asíncronos en C# (introducidos con IAsyncEnumerable<T>
e IAsyncEnumerator<T>
) permiten el procesamiento de flujos de datos de forma asíncrona. Esto permite iterar a través de una secuencia de datos donde cada elemento puede tardar algún tiempo en producirse, sin bloquear el hilo que realiza la llamada. Esto es particularmente útil cuando se trata de operaciones vinculadas a E/S, como leer de una base de datos, un flujo de red o un archivo. Ayudan a hacer un mejor uso de los recursos y a reducir los tiempos de espera, particularmente para conjuntos de datos grandes.
En lugar de usar un bucle foreach
estándar en un IEnumerable<T>
, usas await foreach
en un IAsyncEnumerable<T>
. Aquí hay un ejemplo básico:
async IAsyncEnumerable<int> GenerateNumbers() { for (int i = 0; i < 10; i++) { await Task.Delay(100); // Simula una operación asíncrona yield return i; } } async Task ProcessNumbers() { await foreach (var number in GenerateNumbers()) { Console.WriteLine(number); } }
Elementos clave:
IAsyncEnumerable<T>
: Representa una secuencia asíncrona de datos.IAsyncEnumerator<T>
: Proporciona el mecanismo para iterar sobre la secuencia asíncrona.await foreach
: Se usa para iterar asíncronamente sobre el flujo.
20. Describa el propósito de la clase HttpClient
en C#, y cómo se puede usar para hacer peticiones HTTP.
La clase HttpClient
en C# proporciona una clase base para enviar peticiones HTTP y recibir respuestas HTTP de un recurso identificado por una URI. Actúa como un cliente para interactuar con servidores HTTP. Simplifica el envío de varias peticiones HTTP como GET, POST, PUT, DELETE, etc.
Para usar HttpClient
, se crea una instancia de la misma, se configura con ajustes como la dirección base o encabezados predeterminados si es necesario, y luego se usan sus métodos (por ejemplo, GetAsync
, PostAsync
) para enviar peticiones. Estos métodos normalmente devuelven un Task<HttpResponseMessage>
, lo que permite operaciones asíncronas. El mensaje de respuesta puede entonces ser analizado para recuperar el contenido, los encabezados y el código de estado. Por ejemplo:
using System.Net.Http; using System.Threading.Tasks; public class Example { public static async Task Main(string[] args) { using (HttpClient client = new HttpClient()) { client.BaseAddress = new Uri("https://example.com"); HttpResponseMessage response = await client.GetAsync("/api/data"); if (response.IsSuccessStatusCode) { string result = await response.Content.ReadAsStringAsync(); System.Console.WriteLine(result); } } } }
21. ¿Qué son los atributos personalizados y cómo los implementaría y usaría para el manejo de metadatos personalizados en sus aplicaciones?
Los atributos personalizados, también conocidos como anotaciones en algunos lenguajes, son una forma de agregar metadatos a elementos de código como clases, métodos, propiedades y campos. Permiten asociar información con estos elementos a los que se puede acceder en tiempo de ejecución usando reflexión o en tiempo de compilación usando generadores de código fuente.
Para implementar atributos personalizados, primero define una clase que hereda de la clase base Attribute
. Luego, puede aplicar este atributo a elementos de código usando la sintaxis [NombreAtributo]
. Para usar los metadatos, normalmente usaría reflexión para inspeccionar el elemento de código y recuperar la instancia del atributo. Aquí hay un ejemplo en C#:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class MyCustomAttribute : Attribute { public string Description { get; set; } public MyCustomAttribute(string description) { Description = description; } } [MyCustomAttribute("Esta es una clase de ejemplo")] public class MyClass { [MyCustomAttribute("Este es un método de ejemplo")] public void MyMethod() { } } //accediendo al Attribute: Type type = typeof(MyClass); MyCustomAttribute attribute = (MyCustomAttribute)Attribute.GetCustomAttribute(type, typeof(MyCustomAttribute)); if (attribute != null) { Console.WriteLine(attribute.Description); }
22. Explique diferentes formas de manejar errores y excepciones en C#, incluyendo bloques try-catch, excepciones personalizadas y manejo global de excepciones.
C# ofrece varios mecanismos para manejar errores y excepciones. La forma principal es usar bloques try-catch
. El código que podría lanzar una excepción se coloca dentro del bloque try
. Si ocurre una excepción, el control se transfiere al bloque catch
correspondiente, donde puede manejar la excepción (por ejemplo, registrarla, mostrar un mensaje de error o intentar la recuperación). Múltiples bloques catch
pueden manejar diferentes tipos de excepciones. Se puede agregar un bloque finally
para asegurar que cierto código (por ejemplo, liberar recursos) siempre se ejecute, independientemente de si se lanzó o capturó una excepción.
También puedes crear excepciones personalizadas heredando de la clase Exception
. Esto te permite definir tipos de excepción específicos para las necesidades de tu aplicación. Finalmente, se puede implementar el manejo global de excepciones para capturar excepciones no controladas a nivel de la aplicación. Esto generalmente implica suscribirse a eventos como Application.ThreadException
(Windows Forms) o AppDomain.UnhandledException
(aplicaciones de consola) para registrar o manejar excepciones que no fueron capturadas por bloques try-catch
. Esto se usa a menudo para evitar bloqueos de la aplicación y para proporcionar informes de errores más elegantes. throw;
se puede usar para volver a lanzar la excepción para que pueda ser manejada por capas superiores.
23. ¿Puedes discutir el propósito y los beneficios de usar estructuras de datos inmutables en C# para programación concurrente?
Las estructuras de datos inmutables en C# ofrecen beneficios significativos para la programación concurrente. Su propósito principal es prevenir condiciones de carrera y corrupción de datos al asegurar que, una vez creado, el estado del objeto no se pueda cambiar. Esto elimina la necesidad de bloqueos u otros mecanismos de sincronización cuando múltiples hilos acceden a los mismos datos, simplificando el código concurrente y mejorando el rendimiento.
Los beneficios clave incluyen:
- Seguridad de subprocesos: No se necesitan bloqueos, lo que reduce la complejidad y mejora el rendimiento.
- Predecibilidad: Más fácil de razonar sobre el estado de la aplicación.
- Errores reducidos: Elimina una fuente común de errores de concurrencia.
- Pruebas simplificadas: Más fácil de probar código concurrente sin condiciones de carrera.
Por ejemplo, usar ImmutableList<T>
evita problemas en comparación con List<T>
donde las modificaciones concurrentes sin el bloqueo adecuado pueden conducir a un comportamiento inesperado.
24. ¿Cómo C# soporta la interoperabilidad con código no administrado y cuáles son los desafíos asociados con ella?
C# soporta la interoperabilidad con código no administrado principalmente a través de Platform Invoke (P/Invoke) y COM Interop. P/Invoke permite que el código C# llame a funciones en DLLs escritas en lenguajes como C o C++. COM Interop permite que el código C# interactúe con componentes COM. Para usar P/Invoke, se declara la función externa usando el atributo DllImport
, especificando el nombre de la DLL y otros detalles. Para COM Interop, puede importar bibliotecas de tipos COM como ensamblados .NET usando herramientas como tlbimp.exe
.
Los desafíos incluyen:
- Organización de datos (marshalling): Convertir tipos de datos entre entornos administrados (.NET) y no administrados puede ser complejo y propenso a errores. Una organización incorrecta de datos puede provocar fallos o datos incorrectos.
- Gestión de memoria: El código no administrado a menudo requiere gestión manual de la memoria, lo que puede provocar fugas de memoria o corrupción si no se gestiona cuidadosamente.
- Manejo de excepciones: Las excepciones lanzadas en código no administrado no se propagan automáticamente al entorno administrado, lo que requiere un manejo explícito.
- Seguridad: Interactuar con código no administrado puede introducir vulnerabilidades de seguridad si el código no administrado no es confiable o contiene vulnerabilidades.
25. Explique los conceptos de multihilo y paralelismo en C#, y ¿cómo los implementaría para mejorar el rendimiento?
La multihilo y el paralelismo son técnicas para lograr la concurrencia, pero difieren en su ejecución. La multihilo implica múltiples hilos que se ejecutan dentro de un solo proceso, compartiendo el mismo espacio de memoria. Esto es útil para tareas de entrada/salida (I/O) o tareas que se pueden dividir en unidades de trabajo más pequeñas e independientes. El paralelismo, por otro lado, implica ejecutar múltiples tareas simultáneamente en múltiples núcleos de CPU. Esto es adecuado para tareas de CPU que se pueden dividir en subtareas independientes.
Para implementarlos en C#, puedes usar el espacio de nombres System.Threading
para la ejecución de múltiples hilos (multi-threading) y el espacio de nombres System.Threading.Tasks
, especialmente la clase Parallel
, para el paralelismo. Aquí hay un ejemplo simple usando Parallel.For
para el paralelismo:
Parallel.For(0, 100, i => { // Código a ejecutar en paralelo para cada valor de i Console.WriteLine($"Tarea {i} ejecutándose en el hilo {Thread.CurrentThread.ManagedThreadId}"); });
Para la ejecución de múltiples hilos, puedes usar la clase Thread
o la clase ThreadPool
. Por ejemplo:
Thread thread = new Thread(() => { // Código a ejecutar en un hilo separado Console.WriteLine($"Tarea ejecutándose en el hilo {Thread.CurrentThread.ManagedThreadId}"); }); thread.Start();
Para mejorar el rendimiento, identifica las tareas que pueden ejecutarse concurrentemente y elige la técnica adecuada (multi-threading o paralelismo) según si la tarea es de entrada/salida (I/O-bound) o de procesamiento intensivo (CPU-bound). También, considera factores como la sincronización de hilos y el intercambio de datos para evitar condiciones de carrera (race conditions) y bloqueos (deadlocks).
Preguntas de entrevista de expertos en C#
1. Explica los matices de usar `async` y `await` en una aplicación C# compleja. ¿Cómo aseguras un manejo adecuado de errores y evitas los bloqueos?
Usar async
y await
en aplicaciones C# complejas implica comprender su impacto en el contexto del hilo y la sincronización. async
marca un método como asíncrono, permitiendo el uso de await
. await
suspende la ejecución hasta que una tarea esperada se completa, devolviendo el control al llamador sin bloquear el hilo. Esto es crucial para la capacidad de respuesta de la interfaz de usuario y la escalabilidad.
El manejo de errores es esencial. Usa bloques try-catch
alrededor de las expresiones await
. Por ejemplo:
async Task MyAsyncMethod() { try { await SomeTaskAsync(); } catch (Exception ex) { // Maneja la excepción } }
Para evitar interbloqueos, evite .Result
o .Wait()
en objetos Task
en métodos async
(o métodos llamados desde métodos async
). Estos bloquean el hilo llamante, lo que podría llevar a un interbloqueo si la tarea esperada intenta acceder al contexto del hilo bloqueado. ConfigureAwait(false) puede mitigar los interbloqueos cuando se trata de contextos de la interfaz de usuario, lo que permite que la continuación se ejecute en un hilo del grupo de hilos. Favorezca el enfoque async hasta el final. Cuando una API solo proporciona métodos síncronos, considere la posibilidad de envolverlos en Task.Run
.
2. Describa escenarios en los que usaría un TaskScheduler
personalizado en C#. Explique en qué se diferencia del programador predeterminado y los beneficios que proporciona.
Un TaskScheduler
personalizado en C# es útil cuando necesita un control preciso sobre cómo y dónde se ejecutan las tareas. Los escenarios incluyen: limitar la concurrencia (por ejemplo, asegurar que solo se ejecuten N tareas simultáneamente), ejecutar tareas en un hilo específico (por ejemplo, un hilo de la interfaz de usuario para actualizar la interfaz de usuario) o priorizar tareas según criterios personalizados. Por ejemplo, un motor de juego podría usar un programador personalizado para priorizar las tareas de renderizado sobre el procesamiento en segundo plano.
El TaskScheduler
predeterminado (ThreadPoolTaskScheduler) utiliza el grupo de subprocesos de .NET. Un programador personalizado difiere al permitirle dictar el entorno de ejecución y el orden. Los beneficios son un mayor control sobre el uso de recursos, una mejor capacidad de respuesta (al priorizar tareas críticas) y la capacidad de integrarse con modelos de subprocesos específicos (como STA para aplicaciones de interfaz de usuario). Aquí hay un ejemplo:
public class LimitedConcurrencyTaskScheduler : TaskScheduler { // Detalles de implementación para la gestión de la concurrencia }
3. ¿Cómo maneja el compilador de C# las clausuras y cuáles son los posibles inconvenientes que debe tener en cuenta al usarlas extensivamente?
El compilador de C# maneja las clausuras generando una clase para contener las variables capturadas. Esta clase, a veces llamada "clase de visualización", contiene campos para cualquier variable del alcance externo que se utiliza dentro de la expresión lambda o el método anónimo. Cuando se crea la clausura, se crea una instancia de esta clase de visualización, y las variables capturadas se almacenan dentro de ella. Las ejecuciones posteriores de la clausura acceden entonces a estas variables capturadas a través de la instancia de la clase de visualización.
Un error común al usar cierres extensivamente, especialmente dentro de bucles, es el "problema de la variable capturada". Si una variable de bucle se captura directamente, todos los cierres creados dentro del bucle terminarán referenciando la misma instancia de variable. Esto puede llevar a un comportamiento inesperado donde todos los cierres usan el valor final de la variable del bucle, en lugar del valor en el momento en que se creó cada cierre. Para evitar esto, cree una variable local dentro del bucle y asigne el valor de la variable del bucle a ella. Capture esta variable local en su lugar. for (int i = 0; i < 10; i++) { int temp = i; actions.Add(() => Console.WriteLine(temp)); }
4. Discuta las ventajas y desventajas entre usar structs y clases en C#, centrándose en la asignación de memoria, el rendimiento y los posibles problemas de boxing/unboxing.
Los structs son tipos de valor y se asignan en la pila (o en línea dentro de los tipos contenedores), mientras que las clases son tipos de referencia asignados en el montón. Esto significa que los structs tienen una asignación y desasignación más rápidas y pueden conducir a un mejor rendimiento para objetos pequeños y de corta duración. Sin embargo, copiar un struct implica copiar todos sus datos, lo que puede ser costoso para structs grandes. Las clases solo copian la referencia. Una preocupación clave de rendimiento con los structs es el boxing/unboxing. Cuando un struct necesita ser tratado como un objeto (por ejemplo, pasado a un método que espera System.Object
o una interfaz implementada por el struct), se convierte en un objeto (envuelto) en el montón. El unboxing invierte esto, ambos involucran sobrecarga de rendimiento. Las clases no tienen este problema, ya que ya son tipos de referencia.
En resumen, use structs para estructuras de datos pequeñas e inmutables donde el rendimiento es crítico y desea evitar la asignación de montones, y el boxing se minimiza. Use clases para objetos más complejos con estado mutable, herencia, y cuando se prefiere pasar/devolver por referencia. Considere la huella de memoria de los structs si son grandes, o se copian con frecuencia, ya que las copias grandes de structs pueden negar los beneficios de rendimiento.
5. Explique el funcionamiento interno del recolector de basura de C#. ¿Cómo puede perfilar y optimizar su código para reducir la presión de la recolección de basura?
El recolector de basura (GC) de C# es un administrador de memoria automático. Reclama la memoria ocupada por objetos que ya no están en uso. Es generacional, lo que significa que categoriza los objetos en función de su edad. Gen 0 contiene objetos de corta duración, Gen 1 contiene objetos que sobrevivieron a una recolección de Gen 0 y Gen 2 contiene objetos de larga duración. El GC funciona identificando los objetos raíz (variables estáticas, objetos en la pila) y recorriendo el gráfico de objetos para encontrar los objetos alcanzables. Los objetos inalcanzables se consideran basura y se reclama su memoria. El proceso de GC implica marcar los objetos alcanzables, reubicarlos para compactar la memoria (principalmente para gen 0 y 1) y actualizar las referencias, y barrer (reclamar la memoria de los objetos inalcanzables).
Para reducir la presión de la recolección de basura, puede perfilar su código utilizando herramientas como el Monitor de rendimiento de .NET o las herramientas de diagnóstico de Visual Studio para identificar dónde ocurren las asignaciones con mayor frecuencia. Las técnicas de optimización incluyen: agrupación de objetos para reutilizar objetos en lugar de crear otros nuevos, reducir la vida útil de los objetos eliminándolos rápidamente (usando declaraciones using
o IDisposable
), minimizar las operaciones de boxing y unboxing (ya que estas crean nuevos objetos), usar structs en lugar de clases para objetos pequeños de tipo valor y evitar la concatenación excesiva de cadenas (use StringBuilder
en su lugar). Comprender los tipos de valor frente a los tipos de referencia es crucial. Por ejemplo:
// Malo: Crea muchos objetos de cadena string result = ""; for (int i = 0; i < 1000; i++) { result += i.ToString(); } // Bueno: Usa StringBuilder StringBuilder sb = new StringBuilder(); for (int i = 0; i < 1000; i++) { sb.Append(i.ToString()); } string result = sb.ToString();
6. Describa los detalles de implementación de la ejecución diferida de LINQ. ¿Cómo afecta al rendimiento y la depuración, y cómo puede optimizar las consultas LINQ?
La ejecución diferida de LINQ significa que una consulta no se ejecuta cuando la define. En cambio, se ejecuta cuando itera sobre los resultados (por ejemplo, usando un bucle foreach
o llamando a métodos como ToList()
o ToArray()
). La expresión de consulta se traduce en un árbol de expresiones, que luego es ejecutado por el proveedor LINQ cuando se necesitan los resultados. Esto permite optimizaciones, ya que el proveedor puede analizar toda la consulta y elegir la estrategia de ejecución más eficiente. También permite la composición de consultas, donde se pueden encadenar múltiples consultas y ejecutarse como una sola operación.
La ejecución diferida puede afectar el rendimiento. Si los datos de origen cambian entre la definición y la ejecución de la consulta, los resultados reflejarán los cambios. Además, la iteración repetida sobre la misma consulta la volverá a ejecutar cada vez, lo que podría causar problemas de rendimiento. La depuración puede ser complicada porque la ejecución real de la consulta ocurre más tarde, lo que dificulta el seguimiento de los valores en el momento de la definición. Para optimizar las consultas LINQ: (1) Use ToList()
o ToArray()
si necesita almacenar en caché los resultados y evitar la reejecución. (2) Filtre los datos lo antes posible en la consulta para reducir la cantidad de datos procesados. (3) Considere el uso de consultas compiladas para consultas que se ejecutan con frecuencia, ya que pueden mejorar el rendimiento al precompilar el árbol de expresiones. (4) Tenga en cuenta la tecnología de acceso a datos subyacente (por ejemplo, Entity Framework) y optimice las consultas de la base de datos en consecuencia. Por ejemplo, especificar explícitamente qué columnas seleccionar de la tabla para evitar la obtención de datos innecesarios.
7. ¿Cuáles son las ventajas y desventajas de usar estructuras de datos inmutables en C#? ¿Cómo se implementan eficazmente y cómo se manejan las actualizaciones?
Las estructuras de datos inmutables en C# ofrecen varias ventajas: son inherentemente seguras para subprocesos, lo que simplifica la programación concurrente; mejoran la previsibilidad al evitar cambios inesperados de estado; y pueden mejorar el rendimiento mediante técnicas como el intercambio estructural. Sin embargo, también tienen desventajas: cada modificación crea un nuevo objeto, lo que puede aumentar el consumo de memoria y la sobrecarga de la recolección de basura. Crear y manipular objetos inmutables a veces puede ser menos eficiente que trabajar directamente con objetos mutables, especialmente cuando se trata de grandes conjuntos de datos o actualizaciones frecuentes.
Para implementar eficazmente estructuras de datos inmutables en C#, utilice funciones como campos readonly
y propiedades con solo un getter, lo que evita la modificación externa. Para manejar las actualizaciones, evite la modificación directa; en su lugar, cree nuevas instancias con los cambios deseados, a menudo utilizando métodos que devuelven una copia modificada. Bibliotecas como System.Collections.Immutable
proporcionan colecciones inmutables preconstruidas que optimizan el rendimiento y el uso de la memoria a través del intercambio estructural. Ejemplo: ImmutableList<int> newList = originalList.Add(5);
8. Explique los casos de uso de diferentes tipos de colecciones en C# (por ejemplo, ConcurrentDictionary
, ImmutableList
). ¿Cuándo debería preferir uno sobre otro?
C# ofrece varios tipos de colecciones optimizadas para diferentes escenarios. List<T>
es un array de propósito general y de tamaño variable, adecuado cuando necesita almacenamiento ordenado y acceso frecuente por índice, pero no es seguro para subprocesos. Dictionary<TKey, TValue>
proporciona búsquedas rápidas por clave, ideal para pares clave-valor, pero tampoco es seguro para subprocesos para el acceso concurrente.
ConcurrentDictionary<TKey, TValue>
está diseñado para operaciones seguras para subprocesos en entornos multi-hilo, evitando la corrupción de datos. ImmutableList<T>
crea una nueva lista cada vez que se modifica, conservando así los estados anteriores. Las colecciones inmutables garantizan que los datos no se modifiquen intencionadamente, lo que puede ayudar en gran medida a la depuración y al razonamiento sobre el código, especialmente en escenarios concurrentes o multi-hilo o cuando necesita asegurar que el historial de sus colecciones permanezca intacto.
9. ¿Cómo maneja el runtime de C# la invocación de métodos dinámicos? ¿Cuáles son las implicaciones de rendimiento y los casos de uso para la programación dinámica en C#?
C# maneja la invocación de métodos dinámicos utilizando la palabra clave dynamic
y el Dynamic Language Runtime (DLR). Cuando una variable se declara como dynamic
, la comprobación de tipos se difiere hasta el tiempo de ejecución. Esto permite llamar a métodos y acceder a propiedades que podrían no existir en tiempo de compilación. El DLR utiliza la reflexión y otros mecanismos para resolver la llamada al método en tiempo de ejecución. Si se encuentra el método, se invoca; de lo contrario, se lanza una RuntimeBinderException
.
Las implicaciones de rendimiento de la invocación de métodos dinámicos pueden ser significativas. Debido a que la comprobación de tipos y la resolución de métodos ocurren en tiempo de ejecución, es más lento que el código con tipos estáticos. Sin embargo, la programación dinámica es útil cuando se trabaja con objetos COM, lenguajes de scripting o situaciones donde la estructura del objeto no se conoce en tiempo de compilación, como interactuar con API externas o fuentes de datos donde el esquema podría cambiar.
10. Discuta los patrones de diseño aplicables para construir una arquitectura de microservicios escalable y mantenible utilizando C# y .NET.
Varios patrones de diseño son beneficiosos para construir una arquitectura de microservicios escalable y mantenible con C# y .NET. CQRS (Separación de Responsabilidad de Comandos y Consultas) puede separar las operaciones de lectura y escritura, permitiendo que cada una se escale y se optimice de forma independiente. La Consistencia Eventual complementa CQRS, aceptando que los datos entre servicios podrían no ser inmediatamente consistentes, mejorando la disponibilidad y la capacidad de respuesta. El patrón Saga gestiona las transacciones distribuidas a través de múltiples servicios, garantizando la consistencia de los datos en flujos de trabajo complejos, a menudo implementado utilizando orquestadores o coreografías.
Además, la Puerta de Enlace API proporciona un único punto de entrada para los clientes, abstrayendo los microservicios subyacentes y manejando preocupaciones como la autenticación y la limitación de velocidad. El Interruptor de Circuito mejora la resiliencia al prevenir fallos en cascada entre los servicios. Para la detectabilidad y la configuración, patrones como el Descubrimiento de Servicios (por ejemplo, usando Consul o Eureka, aunque menos común en .NET en comparación con soluciones nativas de la nube como Azure Service Fabric o Kubernetes con su descubrimiento integrado) y la Configuración Externalizada (por ejemplo, usando Azure App Configuration o HashiCorp Vault) son cruciales. Considere usar la biblioteca MediatR para implementar la mensajería en proceso entre servicios, lo que promueve el acoplamiento flexible. Finalmente, emplear una robusta estrategia de Observabilidad (registro, rastreo, métricas) utilizando herramientas como Application Insights, Prometheus o Grafana es crucial para monitorear y depurar un sistema distribuido.
11. Explique cómo implementaría un atributo personalizado en C# para hacer cumplir estándares de codificación específicos o realizar una validación en tiempo de compilación.
Para implementar un atributo personalizado en C# para hacer cumplir estándares de codificación o realizar una validación en tiempo de compilación, primero define una clase que hereda de System.Attribute
. Esta clase contendrá las propiedades relacionadas con la validación o el estándar que desea hacer cumplir. Por ejemplo, si desea aplicar una longitud máxima de cadena para las propiedades, el atributo podría contener una propiedad MaxLength
.
Luego, aplique el atributo personalizado a los elementos de código relevantes (clases, propiedades, métodos, etc.). Durante la compilación, puede usar la reflexión o un analizador (analizador Roslyn) para inspeccionar el código, encontrar los atributos y realizar la validación o generación de código deseadas. Para la validación en tiempo de compilación, es preferible un analizador Roslyn, ya que proporciona capacidades más sólidas para señalar problemas de código directamente en el IDE y durante el proceso de compilación. El analizador analizará los elementos de código atribuidos e informará errores o advertencias en función de los criterios definidos en el atributo personalizado. Ejemplo:
[AttributeUsage(AttributeTargets.Property)] public class StringLengthAttribute : Attribute { public int MaxLength { get; set; } public StringLengthAttribute(int maxLength) { MaxLength = maxLength; } } public class MyClass { [StringLength(50)] public string MyProperty { get; set; } }
12. Describa las diferentes formas de implementar la comunicación entre procesos (IPC) en C#. ¿Cuáles son las ventajas y desventajas de cada enfoque?
- Tuberías (con nombre y anónimas): Las tuberías permiten la comunicación entre procesos en la misma máquina. Las tuberías con nombre permiten la comunicación entre procesos no relacionados, mientras que las tuberías anónimas se utilizan típicamente para la comunicación entre un proceso padre e hijo. Son relativamente simples de implementar, pero se adaptan principalmente a la comunicación local. El rendimiento puede ser bueno, pero la seguridad debe ser considerada para las tuberías con nombre.
- Colas de mensajes: Las colas de mensajes facilitan la comunicación asíncrona al permitir que los procesos envíen y reciban mensajes. Son confiables para escenarios donde los procesos pueden no estar en línea simultáneamente. La implementación implica el uso de bibliotecas o servicios de cola de mensajes externos (por ejemplo, RabbitMQ). La sobrecarga y la complejidad son mayores que las tuberías.
- Sockets TCP/IP: Los sockets proporcionan una forma versátil de comunicarse a través de una red (o localmente). Ofrecen flexibilidad para construir protocolos personalizados, pero requieren más código y consideración de problemas relacionados con la red como la latencia y la seguridad.
- Archivos mapeados en memoria: Los archivos mapeados en memoria permiten que múltiples procesos accedan a la misma región de memoria física, ofreciendo un intercambio de datos eficiente. Adecuados para conjuntos de datos grandes y escenarios de alto rendimiento, pero requieren una sincronización cuidadosa para evitar condiciones de carrera. La complejidad en la configuración y la gestión de la memoria puede ser un inconveniente.
- gRPC/WebAPI: Para aplicaciones distribuidas, gRPC o WebAPI sobre HTTP(S) son opciones viables. Proporcionan comunicación estructurada utilizando protocolos como Protocol Buffers o JSON. Si bien ofrecen interoperabilidad y escalabilidad, estos enfoques implican una mayor sobrecarga en comparación con los mecanismos de IPC locales.
13. ¿Cómo optimizaría una aplicación C# para escenarios de alto rendimiento y baja latencia? Considere la subprocesamiento, la gestión de memoria y la comunicación de red.
Para optimizar una aplicación C# para alto rendimiento y baja latencia, se pueden emplear varias estrategias. Para la subprocesamiento, use el ThreadPool
para tareas de corta duración o asincronía basada en Task
(async
/await
) para evitar bloquear el hilo principal. Minimice la contención de bloqueos usando técnicas como estructuras de datos sin bloqueo (por ejemplo, ConcurrentDictionary
) o bloqueo de grano fino. Considere usar Canales para escenarios eficientes de productor-consumidor. Para la gestión de memoria, minimice las asignaciones y desasignaciones, use la agrupación de objetos donde sea apropiado y aproveche las estructuras en lugar de las clases para los tipos de valor cuando sea aplicable para reducir la presión del montón y la sobrecarga de la recolección de basura. Perfile su código para identificar fugas de memoria y puntos críticos de asignación.
Optimice la comunicación de red mediante el uso de sockets asíncronos para E/S sin bloqueo. Utilice HttpClient
para solicitudes HTTP eficientes, estableciendo límites de conexión apropiados. La serialización se puede optimizar utilizando serializadores eficientes como protobuf-net
o System.Text.Json
en lugar del más lento XmlSerializer
. Minimice la cantidad de datos transferidos a través de la red comprimiendo los datos o enviando solo la información necesaria. Considere el uso de técnicas como el almacenamiento en caché para reducir el número de solicitudes de red. El perfilado usando herramientas como PerfView es clave para identificar cuellos de botella.
14. Explique las características avanzadas de los delegados de C#, como los delegados de multidifusión y la covarianza/contravarianza. Proporcione ejemplos de casos de uso.
Los delegados de C# ofrecen características avanzadas más allá de los punteros a funciones básicos. Los delegados de multidifusión permiten que una instancia de delegado contenga referencias a múltiples métodos. Cuando se invoca el delegado, todos los métodos en su lista de invocación se ejecutan secuencialmente. Esto es útil para escenarios como el manejo de eventos, donde varios suscriptores necesitan ser notificados de un evento. Por ejemplo:
delegate void MyDelegate(string message); MyDelegate myDel = null; myDel += Method1; myDel += Method2; myDel("Hola"); // Se llama a Method1 y Method2
La covarianza y la contravarianza permiten asignaciones de delegados más flexibles. La covarianza permite que un delegado que devuelve un tipo más derivado se asigne a un delegado que devuelve un tipo menos derivado. La contravarianza permite que un delegado que acepta un tipo menos derivado como parámetro se asigne a un delegado que acepta un tipo más derivado como parámetro. Estas características se admiten a través de las palabras clave in
y out
en los parámetros de tipo genérico en las declaraciones de delegados. Esto es particularmente útil cuando se trabaja con interfaces y jerarquías de herencia. Ejemplo:
delegar TResult MyFunc<out TResult>(); // Ejemplo de covarianza delegar void MyAction<in T>(T arg); // Ejemplo de contravarianza
15. Describe la implementación y el uso de iteradores personalizados en C#. ¿En qué se diferencian de las implementaciones estándar de `IEnumerable`?
Los iteradores personalizados en C# se implementan utilizando la instrucción yield return
dentro de un método, propiedad o indexador. Esto permite definir una máquina de estados que produce una secuencia de valores a petición, sin necesidad de materializar toda la colección en la memoria. El método debe devolver IEnumerable<T>
, IEnumerator<T>
, IEnumerable
o IEnumerator
. Por ejemplo:
public IEnumerable<int> GetNumbers(int limit) { for (int i = 0; i < limit; i++) { yield return i; } }
Las implementaciones IEnumerable
estándar generalmente implican crear una clase que implementa la interfaz y almacena toda la colección en la memoria. Los iteradores personalizados, por otro lado, difieren la generación de elementos hasta que se solicitan, lo que conduce a un mejor rendimiento y una menor consumo de memoria, especialmente cuando se trata de secuencias grandes o infinitas. Difieren principalmente en su estrategia de ejecución, ejecución diferida versus ejecución ansiosa de colecciones estándar, y los medios por los cuales se almacena la colección, ya sea implícitamente usando yield
o explícitamente como un objeto.
16. Explique el concepto de Span<T> y Memory<T> en C#. ¿Cómo mejoran el rendimiento al trabajar con búferes de memoria?
Span y Memory en C# proporcionan formas eficientes de trabajar con regiones de memoria contiguas (búferes) sin copias innecesarias, mejorando el rendimiento. Span<T>
es una estructura que representa una región contigua de memoria y ofrece una abstracción segura, sin asignación para memoria administrada y no administrada. Es una ref struct, por lo que reside en la pila y tiene limitaciones con respecto a dónde se puede almacenar (por ejemplo, no en campos de clase). Memory<T>
es una estructura que envuelve un Span<T>
o una matriz y se puede almacenar en el montón (heap).
Mejoran el rendimiento al permitir operaciones en una porción de un buffer (por ejemplo, una porción de una matriz o parte de una cadena) sin asignar nueva memoria para copiar los datos. Por ejemplo, en lugar de crear una subcadena asignando una nueva cadena, Span<char>
puede representar una porción de la cadena original. Este enfoque reduce la asignación de memoria y la sobrecarga de la recolección de basura, lo cual es particularmente beneficioso cuando se trata de buffers grandes u operaciones frecuentes. Permiten escenarios de alto rendimiento como el análisis, el procesamiento de datos y el trabajo con flujos de E/S al proporcionar acceso directo a segmentos de memoria sin incurrir en el costo de la copia. Se utilizan con frecuencia con métodos como AsSpan()
para crear un span a partir de la memoria existente.
ReadOnlySpan<T>
y ReadOnlyMemory<T>
son las contrapartes de solo lectura, lo que garantiza que los datos no se modifiquen.
17. ¿Cómo diseñaría una aplicación C# para que sea resistente a fallas transitorias en un entorno distribuido? Considere el uso de Polly o bibliotecas similares.
Para construir una aplicación C# resistente en un entorno distribuido utilizando Polly, implementaría políticas de reintento y patrones de disyuntor (circuit breaker) para manejar fallas transitorias. Las políticas de reintento reintentan automáticamente las operaciones fallidas un número especificado de veces o hasta que se cumple una condición. El patrón de disyuntor evita que una aplicación intente repetidamente ejecutar una operación que probablemente fallará, dando tiempo al servicio descendente para recuperarse. Estas estrategias se implementan utilizando RetryPolicy
y CircuitBreakerPolicy
de Polly.
Para ilustrarlo, considere un escenario en el que la aplicación se comunica con una API remota. Yo encapsularía las llamadas a la API con una política de Polly como esta:
var retryPolicy = Policy.Handle<HttpRequestException>() .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); var circuitBreakerPolicy = Policy.Handle<HttpRequestException>() .CircuitBreakerAsync(3, TimeSpan.FromSeconds(30)); var policyWrap = Policy.WrapAsync(retryPolicy, circuitBreakerPolicy); var result = await policyWrap.ExecuteAsync(() => _httpClient.GetAsync("https://api.example.com/data"));
Esto asegura que los problemas de red transitorios o la falta de disponibilidad temporal de la API no bloqueen la aplicación. Polly gestiona los reintentos y el corte de circuito basándose en las políticas definidas, mejorando la resiliencia de la aplicación. También se podrían considerar otras políticas como Timeout y Fallback.
18. Discuta las diversas formas de serializar y deserializar objetos en C#. ¿Cuáles son las consideraciones de rendimiento y seguridad para cada método?
C# ofrece varias formas de serializar y deserializar objetos, cada una con sus propias ventajas y desventajas. Los métodos comunes incluyen: Serialización binaria (usando BinaryFormatter
), Serialización XML (XmlSerializer
), Serialización JSON (usando System.Text.Json
, Newtonsoft.Json
) y Serialización de contrato de datos (DataContractSerializer
). La serialización binaria es generalmente más rápida, pero presenta riesgos de seguridad significativos, ya que puede ejecutar código arbitrario durante la deserialización; también es dependiente de la versión. La serialización XML es más segura e interoperable, pero puede ser menos eficiente. La serialización JSON se usa ampliamente debido a su formato legible por humanos y su amplio soporte, ofreciendo un buen rendimiento, especialmente con System.Text.Json
. La serialización de contrato de datos proporciona un equilibrio entre rendimiento y flexibilidad, y es menos sensible a los cambios de versión.
El rendimiento depende del tamaño y la complejidad de los objetos, el serializador elegido y la implementación subyacente. En cuanto a la seguridad, evite usar BinaryFormatter
debido a su vulnerabilidad a los ataques de deserialización. Valide siempre la entrada durante la deserialización para evitar que los datos maliciosos comprometan su aplicación. Considere usar atributos de serialización (por ejemplo, [Serializable]
, [DataContract]
) para controlar qué miembros se serializan y cómo. Al elegir un método, considere factores como el rendimiento, la seguridad, la interoperabilidad y los requisitos de control de versiones.
19. Explique cómo implementaría una fuente y un listener de diagnóstico personalizados en C# para monitorear y diagnosticar problemas de rendimiento en un entorno de producción.
Para implementar una fuente y un listener de diagnóstico personalizados en C#, primero debe definir una subclase DiagnosticSource
para representar los eventos específicos de su aplicación. Esto implica definir métodos que escriben eventos de diagnóstico usando DiagnosticSource.Write()
, incluyendo nombres de eventos y cargas útiles (objetos anónimos o clases personalizadas).
Luego, cree un DiagnosticListener
que se suscribe al DiagnosticSource
. El método OnNext()
del listener se invocará cada vez que la fuente escriba un evento. Dentro de OnNext()
, puede filtrar eventos basándose en su nombre y procesar la carga útil, por ejemplo, registrando los datos en un archivo, enviándolos a un sistema de monitoreo o activando alertas. Configure el DiagnosticListener
para suscribirse a su DiagnosticSource
utilizando la colección observable DiagnosticListener.AllListeners
o un nombre de fuente específico. Esto permite la supervisión y el diagnóstico en tiempo real de los problemas de rendimiento en producción. Ejemplo:
//Fuente de diagnóstico public class MyDiagnosticSource : DiagnosticSource { public override bool IsEnabled(string name) => true; //o lógica personalizada public override void Write(string name, object value) { //escribir eventos } } //Listener de diagnóstico public class MyDiagnosticListener : IObserver<KeyValuePair<string, object>> { public void OnNext(KeyValuePair<string, object> value) { //Procesar el evento } //Otros métodos de implementación de IObserver }
20. Describa el papel de los analizadores de Roslyn y las correcciones de código en la mejora de la calidad del código. ¿Cómo puede crear analizadores personalizados para los estándares de codificación de su equipo?
Los analizadores y correcciones de código de Roslyn desempeñan un papel crucial en la mejora de la calidad del código al hacer cumplir los estándares de codificación, identificar errores potenciales y sugerir mejoras durante el desarrollo. Los analizadores realizan análisis de código estático, examinando el código en busca de violaciones de reglas predefinidas, inconsistencias estilísticas o posibles problemas en tiempo de ejecución, ofreciendo retroalimentación en tiempo real dentro del IDE. Las correcciones de código, por otro lado, brindan soluciones automáticas o semiautomáticas para abordar los problemas señalados por los analizadores, lo que permite a los desarrolladores corregir rápidamente su código y adherirse a las mejores prácticas.
La creación de analizadores Roslyn personalizados implica:
- Definir reglas de diagnóstico que representen los estándares de codificación. Estas reglas especifican qué buscar en el código y la gravedad del problema.
- Implementar un analizador que detecte las violaciones de estas reglas al recorrer el árbol de sintaxis del código.
- Escribir un proveedor de corrección de código que sugiera y aplique transformaciones de código para corregir las violaciones. El analizador y la corrección de código generalmente se empaquetan como un paquete NuGet para facilitar la distribución y el consumo dentro de un equipo u organización. Código de ejemplo:
DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);
21. ¿Cómo implementaría un mecanismo genérico de reintento en C# que maneje diferentes tipos de excepciones e implemente un retroceso exponencial?
Un mecanismo genérico de reintento en C# se puede implementar utilizando una combinación de delegados, filtros de excepciones y un bucle con retroceso exponencial. La idea principal es definir un método que encapsule la operación que se va a reintentar y ejecutarlo dentro de un bloque try-catch
.
Aquí hay un ejemplo simplificado:
public static T Retry<T>(Func<T> operación, int maxRetries = 3, TimeSpan? initialDelay = null, params Type[] exceptionTypes) { initialDelay = initialDelay ?? TimeSpan.FromSeconds(1); exceptionTypes = exceptionTypes ?? new Type[] { typeof(Exception) }; int retryCount = 0; while (true) { try { return operación(); } catch (Exception ex) when (exceptionTypes.Any(type => type.IsInstanceOfType(ex))) { if (retryCount >= maxRetries) throw; retryCount++; TimeSpan delay = initialDelay.Value * Math.Pow(2, retryCount - 1); Thread.Sleep(delay); } } }
22. Explique cómo puede aprovechar las características de C# como los generadores de código fuente para automatizar tareas repetitivas y reducir el código repetitivo en sus proyectos.
Los generadores de código fuente en C# te permiten inspeccionar el código del usuario y generar código fuente C# adicional durante la compilación. Esto es poderoso para automatizar tareas repetitivas. Por ejemplo, podrías generar automáticamente implementaciones para interfaces, crear código estándar para objetos de transferencia de datos (DTO), o generar lógica eficiente de serialización/deserialización. Al mover esta lógica a un generador, evitas escribir y mantener el mismo código en múltiples proyectos, reduciendo código repetitivo y el riesgo de errores.
Específicamente, los generadores de código fuente pueden analizar el código marcado con atributos personalizados, interfaces o convenciones de nomenclatura y luego generar código asociado. Funcionan en tiempo de compilación, mejorando el rendimiento en comparación con los enfoques basados en reflexión en tiempo de ejecución. Algunos ejemplos comunes incluyen la generación de código para implementaciones de INotifyPropertyChanged, contenedores de inyección de dependencias, o la creación automática de código de mapeo entre diferentes tipos. El código generado se agrega a la compilación, por lo que está disponible como código escrito a mano.
C# MCQ
Pregunta 1.
Considera el siguiente código C#:
public class Animal { public virtual string MakeSound() { return "Generic animal sound"; } } public class Dog : Animal { public override string MakeSound() { return "Woof!"; } } public class Cat : Animal { public override string MakeSound() { return "Meow!"; } } public class Program { public static void Main(string[] args) { Animal myAnimal = new Cat(); Console.WriteLine(myAnimal.MakeSound()); } }
¿Cuál será la salida de este programa?
Opciones:
"Generic animal sound"
"Woof!"
"Meow!"
Ocurrirá un error durante la compilación.
Pregunta 2.
¿Cuál de las siguientes afirmaciones es verdadera con respecto a las clases abstractas en C#?
- A) Las clases abstractas se pueden instanciar directamente usando la palabra clave
new
. - B) Las clases abstractas solo pueden contener métodos abstractos.
- C) Las clases abstractas pueden contener métodos abstractos y no abstractos.
- D) Las clases abstractas no pueden heredar de interfaces.
Opciones:
Las clases abstractas se pueden instanciar directamente usando la palabra clave `new`.
Las clases abstractas solo pueden contener métodos abstractos.
Las clases abstractas pueden contener métodos abstractos y no abstractos.
Las clases abstractas no pueden heredar de interfaces.
Pregunta 3.
Considera el siguiente fragmento de código C#:
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 }; var evenNumbers = numbers.Where(n => n % 2 == 0); numbers.Add(6); foreach (var number in evenNumbers) { Console.WriteLine(number); }
¿Cuál será la salida de este código?
Opciones:
2 4
2 4 6
Sin salida
Se lanzará una excepción
Pregunta 4.
¿Cuál de las siguientes afirmaciones describe mejor la relación entre delegados y eventos en C#?
Opciones:
Los eventos son un tipo de delegado que proporciona encapsulación a la lista de invocación del delegado.
Los delegados son un tipo especial de evento que solo admite multidifusión.
Los eventos y los delegados son lo mismo y se pueden usar indistintamente.
Los delegados se utilizan para definir eventos, pero los eventos no están relacionados con los delegados.
Pregunta 5.
¿Cuál es la diferencia clave entre los operadores is
y as
en C#?
Opciones:
`is` lanza una excepción si la conversión falla, mientras que `as` devuelve `null`.
`is` comprueba si un objeto es de un cierto tipo y devuelve un booleano, mientras que `as` intenta convertir un objeto a un tipo especificado y devuelve `null` si la conversión no es posible.
`as` comprueba si un objeto es de un cierto tipo y devuelve un booleano, mientras que `is` intenta convertir un objeto a un tipo especificado y devuelve `null` si la conversión no es posible.
Tanto `is` como `as` realizan la misma función, pero `is` es preferido por razones de rendimiento.
Pregunta 6.
¿Cuál de las siguientes es la forma correcta de definir una clase genérica DataHolder
que solo acepta tipos que implementan la interfaz IComparable
?
Opciones:
```csharp public class DataHolder<T> where T : IComparable { private T data; } ```
```csharp public class DataHolder<T> implements IComparable<T> { private T data; } ```
```csharp public class DataHolder<T> : IComparable<T> { private T data; } ```
```csharp public class DataHolder<T> when T : IComparable { private T data; } ```
Pregunta 7.
¿Cuál de las siguientes afirmaciones describe mejor el propósito de las propiedades en C#?
Opciones:
Las propiedades se utilizan para acceder directamente a las variables miembro privadas de una clase.
Las propiedades proporcionan una forma controlada de acceder y modificar los datos de la clase, utilizando los accesores get y set.
Las propiedades solo se utilizan para definir constantes dentro de una clase.
Las propiedades son métodos especiales que solo se pueden usar en clases abstractas.
Pregunta 8.
¿Cuál es la diferencia clave entre los tipos de valor y los tipos de referencia en C#?
Opciones:
Los tipos de valor se almacenan en la pila, mientras que los tipos de referencia se almacenan en el montón.
Los tipos de valor son inmutables, mientras que los tipos de referencia son mutables.
Los tipos de valor se pasan por referencia, mientras que los tipos de referencia se pasan por valor.
Los tipos de valor pueden ser nulos, mientras que los tipos de referencia no pueden serlo.
Pregunta 9.
Considere el siguiente código C#:
using System; using System.Threading.Tasks; public class Ejemplo { public static async Task Main(string[] args) { Console.WriteLine(await GetValueAsync()); } public static async Task<int> GetValueAsync() { Task<int> task = Task.Run(() => GetValue()); return task.Result; } public static int GetValue() { return 42; } }
¿Cuál es el resultado más probable cuando se ejecuta este código?
a) El programa se ejecutará correctamente y producirá 42
. b) El programa lanzará una excepción. c) El programa se bloqueará, causando que la aplicación se congele. d) El programa mostrará un entero aleatorio.
Opciones:
El programa se ejecutará correctamente y producirá `42`.
El programa lanzará una excepción.
El programa se bloqueará, causando que la aplicación se congele.
El programa mostrará un entero aleatorio.
Pregunta 10.
¿Cuál de las siguientes afirmaciones es la más precisa con respecto a los métodos de extensión de C#?
- a) Los métodos de extensión solo se pueden definir dentro de la misma clase que el tipo que extienden.
- b) Los métodos de extensión le permiten agregar nuevos métodos a tipos existentes sin modificar su definición original.
- c) Los métodos de extensión pueden anular los métodos existentes del tipo que extienden.
- d) Los métodos de extensión solo se pueden aplicar a los tipos de valor, no a los tipos de referencia.
Opciones:
Los métodos de extensión solo se pueden definir dentro de la misma clase que el tipo que extienden.
Los métodos de extensión le permiten agregar nuevos métodos a tipos existentes sin modificar su definición original.
Los métodos de extensión pueden anular los métodos existentes del tipo que extienden.
Los métodos de extensión solo se pueden aplicar a los tipos de valor, no a los tipos de referencia.
Pregunta 11.
Considere el siguiente fragmento de código C#:
intenta { // Algo de código que podría lanzar excepciones string str = null; int longitud = str.Length; } catch (ArgumentNullException ex) { Console.WriteLine("Se capturó ArgumentNullException"); } catch (NullReferenceException ex) { Console.WriteLine("Se capturó NullReferenceException"); } catch (Exception ex) { Console.WriteLine("Se capturó Exception"); }
¿Cuál será la salida de este código?
Opciones:
Se capturó ArgumentNullException
Se capturó NullReferenceException
Se capturó Exception
El programa terminará abruptamente sin capturar ninguna excepción.
Pregunta 12.
¿Cuál es la principal diferencia entre IEnumerable<T>
e IQueryable<T>
en C# con respecto a la recuperación de datos?
Opciones:
`IEnumerable<T>` recupera datos de la base de datos, mientras que `IQueryable<T>` recupera datos de la memoria.
`IQueryable<T>` ejecuta consultas en el lado del servidor, lo que podría optimizar el proceso de recuperación de datos, mientras que `IEnumerable<T>` ejecuta consultas en memoria después de recuperar todos los datos.
`IEnumerable<T>` admite la ejecución diferida, mientras que `IQueryable<T>` se ejecuta inmediatamente.
`IQueryable<T>` se utiliza para colecciones locales, mientras que `IEnumerable<T>` se utiliza para fuentes de datos remotas.
Pregunta 13.
En C#, ¿cuál es la principal diferencia entre una struct
y una class
con respecto a la asignación de memoria y el comportamiento?
Opciones:
Las estructuras son tipos de referencia asignados en el montón (heap), mientras que las clases son tipos de valor asignados en la pila (stack).
Las estructuras son tipos de valor asignados en la pila, y sus valores se copian en la asignación, mientras que las clases son tipos de referencia asignados en el montón, y sus referencias se copian en la asignación.
Las estructuras y las clases son idénticas en términos de asignación de memoria y comportamiento; la única diferencia es la sintaxis.
Las estructuras solo pueden contener tipos de datos primitivos, mientras que las clases pueden contener cualquier tipo de datos, incluidas otras clases y estructuras.
Pregunta 14.
¿Cuál es el propósito principal de la declaración using
en C#, y cómo se relaciona con la interfaz IDisposable
?
- a) Administra automáticamente el ciclo de vida de los recursos, asegurando que se llame a
Dispose()
incluso si ocurren excepciones. - b) Define un alias de espacio de nombres para simplificar la legibilidad del código.
- c) Permite la creación de tipos genéricos con parámetros de tipo.
- d) Proporciona un mecanismo para manejar excepciones dentro de un bloque de código específico.
Opciones:
Administra automáticamente el ciclo de vida de los recursos, asegurando que se llame a Dispose()
incluso si ocurren excepciones.
Define un alias de espacio de nombres para simplificar la legibilidad del código.
Permite la creación de tipos genéricos con parámetros de tipo.
Proporciona un mecanismo para manejar excepciones dentro de un bloque de código específico.
Pregunta 15.
¿Cuál es el principal problema de rendimiento asociado con el boxing y unboxing en C#?
Opciones:
Aumento del uso de memoria debido a la creación de un nuevo objeto en el heap al hacer boxing de un tipo de valor.
Causa problemas con la recolección de basura porque los objetos con boxing no se rastrean correctamente.
Boxing y unboxing impiden el uso de genéricos.
Conduce a errores en tiempo de compilación debido a discrepancias de tipo.
Pregunta 16.
¿Cuál es el propósito principal de la instrucción yield return
en C#?
Opciones:
Para terminar inmediatamente el método actual y devolver un valor al que llama.
Para ejecutar un bloque de código de forma asíncrona.
Para proporcionar un valor al que llama y mantener el estado actual del método, permitiéndole reanudar desde el mismo punto más tarde.
Para declarar un valor constante dentro de un método.
Pregunta 17.
En C#, ¿cuál es el principal beneficio del interning de cadenas, y cuál de los siguientes escenarios ilustra mejor cuándo es más efectivo?
Opciones:
Opciones:
El interning de cadenas reduce principalmente el consumo de memoria al garantizar que solo exista una instancia de una cadena literal con un valor particular en la memoria, lo que la hace más efectiva cuando se trata de cadenas literales o constantes de uso frecuente.
El interning de cadenas acelera principalmente las operaciones de concatenación de cadenas al preasignar memoria para cadenas combinadas, lo que la hace más efectiva al construir cadenas grandes a partir de múltiples cadenas más pequeñas.
El interning de cadenas mejora principalmente el rendimiento de la comparación de cadenas al precalcular códigos hash para cadenas, lo que la hace más efectiva al realizar comprobaciones frecuentes de igualdad en diferentes variables de cadena.
El interning de cadenas mejora principalmente la seguridad al evitar la modificación de literales de cadena en tiempo de ejecución, lo que lo hace más eficaz al manejar datos confidenciales como contraseñas o claves API.
Pregunta 18.
¿Cuál es la forma correcta de crear un Task
cancelable en C# usando un CancellationToken
y asegurar que la tarea respete la solicitud de cancelación?
Opciones:
```csharp CancellationTokenSource cts = new CancellationTokenSource(); Task task = new Task(() => { while (!cts.Token.IsCancellationRequested) { // Realizar trabajo } }, cts.Token); task.Start(); ```
```csharp CancellationTokenSource cts = new CancellationTokenSource(); Task task = Task.Run(() => { cts.Token.ThrowIfCancellationRequested(); // Realizar trabajo }, cts.Token); ```
```csharp CancellationTokenSource cts = new CancellationTokenSource(); Task task = Task.Run(() => { // Realizar trabajo }, cts.Token); cts.Token.Register(() => task.Dispose()); ```
CancellationTokenSource cts = new CancellationTokenSource(); Task tarea = Task.Run(() => { cts.Token.ThrowIfCancellationRequested(); while (!cts.Token.IsCancellationRequested) { // Realizar trabajo } cts.Token.ThrowIfCancellationRequested(); }, cts.Token); Pregunta 19. ¿Cuál de las siguientes afirmaciones describe con precisión la diferencia clave entre la vinculación temprana (vinculación estática) y la vinculación tardía (vinculación dinámica) en C#? a) La vinculación temprana resuelve las llamadas a métodos en tiempo de compilación, mientras que la vinculación tardía las resuelve en tiempo de ejecución. b) La vinculación temprana admite el polimorfismo a través de la herencia, mientras que la vinculación tardía no. c) La vinculación temprana se utiliza principalmente con interfaces, mientras que la vinculación tardía se utiliza con clases abstractas. d) La vinculación temprana permite actualizaciones dinámicas de código sin recompilación, mientras que la vinculación tardía requiere recompilación. Opciones: La vinculación temprana resuelve las llamadas a métodos en tiempo de compilación, mientras que la vinculación tardía las resuelve en tiempo de ejecución. La vinculación temprana admite el polimorfismo a través de la herencia, mientras que la vinculación tardía no. La vinculación temprana se utiliza principalmente con interfaces, mientras que la vinculación tardía se utiliza con clases abstractas. La vinculación temprana permite actualizaciones dinámicas de código sin recompilación, mientras que la vinculación tardía requiere recompilación. Pregunta 20. Considere el siguiente fragmento de código C#: Dictionary<string, int> miDict = new Dictionary<string, int>(); miDict.Add("manzana", 1); // ¿Qué sucederá si intenta agregar otro par clave/valor donde la clave ya existe? miDict.Add("manzana", 2);
¿Cuál de las siguientes afirmaciones es la más precisa sobre el resultado de ejecutar la línea myDict.Add("apple", 2);
?
Opciones:
El valor asociado con la clave "apple" se actualizará a 2.
El código compilará y se ejecutará, pero el diccionario contendrá dos entradas con la clave "apple".
Se lanzará una `ArgumentException` porque ya existe una clave con el mismo nombre en el diccionario.
El código compilará sin error y no se lanzará ninguna excepción, conservando el valor original para `apple`.
Pregunta 21.
¿Cuál de las siguientes afirmaciones describe MEJOR la relación entre los métodos anónimos y las expresiones lambda en C#?
- (A) Los métodos anónimos son una sintaxis más concisa para las expresiones lambda.
- (B) Las expresiones lambda son una sintaxis más concisa para los métodos anónimos.
- (C) Los métodos anónimos y las expresiones lambda son intercambiables y tienen una funcionalidad idéntica.
- (D) Las expresiones lambda solo se pueden usar con los delegados Func y Action, mientras que los métodos anónimos no tienen tal restricción.
Opciones:
Los métodos anónimos son una sintaxis más concisa para las expresiones lambda.
Las expresiones lambda son una sintaxis más concisa para los métodos anónimos.
Los métodos anónimos y las expresiones lambda son intercambiables y tienen una funcionalidad idéntica.
Las expresiones lambda solo se pueden usar con los delegados Func y Action, mientras que los métodos anónimos no tienen tal restricción.
Pregunta 22.
¿Cuál de las siguientes describe mejor el propósito principal de Reflection en C#?
Opciones:
Para habilitar la comprobación de errores en tiempo de compilación y mejorar el rendimiento del código.
Para descubrir, inspeccionar y manipular dinámicamente tipos y miembros en tiempo de ejecución.
Para proporcionar un mecanismo para crear interfaces de usuario mediante la funcionalidad de arrastrar y soltar.
Para generar automáticamente documentación para el código C#.
Pregunta 23.
¿Cuál es el propósito principal del operador de fusión en null (??
) en C# cuando se usa con tipos que admiten valores null?
Opciones:
Para convertir automáticamente un tipo que admite valores null a su tipo subyacente que no los admite, generando una excepción si el tipo que admite valores null es null.
Para proporcionar un valor predeterminado cuando un tipo que admite valores null tiene un valor null.
Para verificar si un tipo que admite valores null es null y devolver un valor booleano.
Para asignar explícitamente un valor null a un tipo que admite valores null.
Pregunta 24.
¿Qué afirmación describe mejor el propósito principal de los atributos en C#?
Opciones:
Los atributos se utilizan para definir tipos de datos personalizados.
Los atributos proporcionan metadatos sobre el código para ser utilizados en tiempo de compilación o tiempo de ejecución.
Los atributos se utilizan para hacer cumplir restricciones de seguridad en clases y métodos.
Los atributos se utilizan para optimizar el rendimiento de la aplicación.
Pregunta 25.
En C#, ¿en qué circunstancias se garantiza que se llamará al finalizador de un objeto?
Opciones:
Cuando el objeto sale del ámbito.
Cuando el método `Dispose()` se llama explícitamente en el objeto.
Cuando el recolector de basura determina que el objeto ya no es accesible y ejecuta el finalizador en un hilo separado.
Inmediatamente antes de que la aplicación finalice, independientemente del estado del objeto.
¿Qué habilidades de C# debería evaluar durante la fase de entrevista?
Evaluar las habilidades de C# de un candidato en una sola entrevista es un desafío, ya que no es posible evaluar todo. Sin embargo, centrarse en las habilidades básicas de C# garantiza que se identifiquen a los candidatos con la experiencia adecuada. Estas habilidades son esenciales para el éxito en los roles de desarrollo de C#.
Programación Orientada a Objetos (POO)
La Programación Orientada a Objetos es una habilidad importante. Puedes usar una prueba de C# que incluya preguntas de opción múltiple (MCQ) relevantes para evaluar rápidamente la comprensión de un candidato sobre los conceptos de POO, como la herencia, el polimorfismo y la encapsulación. Esto puede ahorrar tiempo y ayudar a filtrar candidatos.
Para evaluar su comprensión práctica, puedes hacer una pregunta de entrevista específica.
Describe la diferencia entre herencia y composición. ¿Cuándo usarías una sobre la otra?
Busca una respuesta que muestre que entienden los beneficios de ambas. Idealmente, deberían mencionar que la composición permite una mayor flexibilidad y evita el problema de la 'clase base frágil'.
.NET Framework y Core
La evaluación de su conocimiento de .NET se puede hacer eficazmente con una evaluación de .NET. Estas pruebas cubren las características, bibliotecas y herramientas relevantes dentro del framework .NET, lo que te ayuda a filtrar a los candidatos con una base sólida.
Una pregunta de entrevista específica puede revelar aún más su profundidad de comprensión.
Explica la diferencia entre .NET Framework y .NET Core (ahora .NET). ¿Cuáles son las ventajas clave de .NET Core?
El candidato debe mencionar las capacidades multiplataforma, la modularidad y las mejoras de rendimiento. Su respuesta debe mostrar una conciencia de la evolución del ecosistema .NET.
Programación asíncrona
Puedes usar una evaluación de C# para filtrar candidatos en esta habilidad particular. Puedes evaluar a los candidatos en conceptos como async/await y la Biblioteca de tareas paralelas (Task Parallel Library).
Para evaluar su experiencia práctica, plantea una pregunta que les exija explicar el concepto.
Describe un escenario en el que usarías la programación asíncrona en una aplicación C#. Explica cómo funcionan las palabras clave async
y await
en ese contexto.
Busca una comprensión de cómo las operaciones asíncronas evitan bloquear el hilo principal. Deben demostrar la capacidad de describir un caso de uso del mundo real, como manejar solicitudes de red o realizar tareas en segundo plano.
LINQ (Consulta integrada de lenguaje)
Evalúa sus habilidades de LINQ con una evaluación de LINQ específica. Dicha prueba puede determinar rápidamente si comprenden la sintaxis y los conceptos de LINQ, lo que ayuda a filtrar candidatos de manera efectiva.
Haz una pregunta que les impulse a explicar el propósito de LINQ.
Explica qué es LINQ y proporciona un ejemplo de cómo lo has usado en un proyecto.
El candidato debe demostrar una comprensión del propósito de LINQ en la consulta de datos. Busca un ejemplo claro de cómo han aplicado LINQ para simplificar el acceso y la manipulación de datos.
Contrata a desarrolladores de C# calificados con evaluaciones específicas y entrevistas perspicaces
Al contratar desarrolladores de C#, es importante evaluar con precisión sus habilidades para garantizar que cumplan con los requisitos de tu proyecto. Verificar su experiencia en conceptos de C# y prácticas de desarrollo es el primer paso.
La forma más confiable de evaluar la competencia de un candidato en C# es a través de pruebas de habilidades. Explore la gama de evaluaciones de Adaface, incluyendo el Test Online de C# y el Test Online de .NET, para identificar a los mejores talentos.
Después de las pruebas, puede filtrar y preseleccionar fácilmente a los candidatos más prometedores. Esto le permite centrar sus esfuerzos de entrevista en aquellos que han demostrado las habilidades de C# más sólidas.
¿Listo para optimizar su proceso de contratación de desarrolladores C#? Regístrese para una prueba gratuita en Adaface o aprenda más sobre nuestras Pruebas de Codificación.
Test Online de C#
40 minutos | 10 preguntas de opción múltiple y 1 pregunta de codificación
El test de C# tiene preguntas de opción múltiple basadas en escenarios para evaluar los conceptos básicos de C# (variables, funciones, tipos, etc.), los conceptos de POO de C# (clases y patrones de diseño), el uso eficiente de C# (manejo de excepciones, recolección de basura, etc.) y la capacidad de escalar programas C# utilizando programación asíncrona. El test utiliza preguntas de codificación simples para evaluar el conocimiento práctico de la programación en C#.
[
Prueba el Test Online de C#
](https://www.adaface.com/assessment-test/c-sharp-online-test)
Descargue la plantilla de preguntas de entrevista de C# en múltiples formatos
Preguntas frecuentes sobre entrevistas de C#
Las preguntas básicas de la entrevista de C# cubren temas como tipos de datos, operadores, flujo de control y principios básicos de programación orientada a objetos.
Las preguntas intermedias de la entrevista de C# exploran temas como delegados, eventos, LINQ, manejo de excepciones y colecciones.
Las preguntas avanzadas de la entrevista de C# cubren temas como programación asíncrona, reflexión, genéricos, atributos y administración de memoria.
Las preguntas de la entrevista de C# para expertos profundizan en escenarios complejos, patrones de diseño, optimización del rendimiento y una comprensión profunda del framework .NET.
Combine evaluaciones específicas con preguntas de entrevista perspicaces para evaluar las habilidades, las habilidades de resolución de problemas y el conocimiento práctico de C# de un candidato.
Next posts
- Plantillas de correo electrónico
- ¿Cómo contratar a un ingeniero de la nube de Azure: habilidades, consejos y una guía paso a paso?
- Cómo contratar a ingenieros de operaciones de aprendizaje automático (MLOps): Una guía completa
- Cómo contratar a un desarrollador de infraestructura de TI: consejos, conocimientos y una guía paso a paso
- Cómo Contratar a un Gerente de Cuentas de Ventas: Una Guía Paso a Paso para Reclutadores