Todo el código de BiblioTech escrito hasta ahora es fuertemente tipado: el compilador
conoce, en cada línea, el tipo exacto de cada variable (Libro, string, List<Socio>...) y
rechaza cualquier operación que no encaje —acceder a una propiedad inexistente, pasar un
argumento del tipo equivocado— antes de que el programa llegue a ejecutarse. C# ofrece, además,
un tipo especial que renuncia deliberadamente a esa comprobación: dynamic. Esta lección
explica qué es, en qué se diferencia de object y de var (con los que se confunde con
frecuencia), cuándo tiene sentido usarlo, y por qué el dominio de BiblioTech, tipado de forma
consciente desde el Módulo 1, debe seguir evitándolo en su código de negocio.
Contenido
- El tipo
dynamic: resolución en tiempo de ejecución dynamicfrente aobject: mismo dato, comprobación en momentos distintosdynamicfrente avar: dinámico no es lo mismo que inferidoExpandoObject: objetos dinámicos sin una clase definida- Cuándo tiene sentido usar
dynamic - Por qué evitar
dynamicen el dominio de BiblioTech - Ejemplo: leer un JSON de estructura desconocida con
dynamic, frente al enfoque tipado
- El tipo
dynamic: resolución en tiempo de ejecución
dynamic: resolución en tiempo de ejecuciónUna variable declarada como dynamic le dice al compilador: "no compruebes nada sobre esta
variable ahora; resuélvelo cuando el programa se ejecute". Cualquier acceso a una propiedad, un
método o un operador sobre una variable dynamic se pospone hasta tiempo de ejecución, momento
en el que el runtime de .NET (el Dynamic Language Runtime, DLR) intenta resolverlo contra el
tipo real del valor que contiene en ese instante:
dynamic valor = "Rayuela";
Console.WriteLine(valor.Length); // 7: en tiempo de ejecucion, "Rayuela" es un string, que tiene Length
valor = 42;
Console.WriteLine(valor.Length); // Excepcion en tiempo de ejecucion: int no tiene una propiedad LengthEl primer Console.WriteLine compila y funciona porque, al ejecutarse, valor contiene un
string, y string sí tiene Length. El segundo compila igual de bien —el compilador no
comprueba nada sobre dynamic— pero falla en tiempo de ejecución, con una
RuntimeBinderException, porque int no tiene ninguna propiedad Length. Este es el coste
central de dynamic: los errores que el compilador detectaría de inmediato sobre cualquier otro
tipo se convierten en excepciones que solo aparecen al ejecutar el programa, quizás mucho después
de haberlo escrito.
dynamic frente a object: mismo dato, comprobación en momentos distintos
dynamic frente a object: mismo dato, comprobación en momentos distintosobject (el tipo más general del sistema de tipos de C#, Módulo 3) y dynamic pueden parecer
similares —ambos pueden contener cualquier valor— pero difieren en cuándo se comprueba el
tipo antes de operar con él:
object comoObject = "Rayuela";
// comoObject.Length; // Error de COMPILACION: object no tiene Length
int longitud = ((string)comoObject).Length; // hace falta un cast explicito primero
dynamic comoDynamic = "Rayuela";
int longitudDinamica = comoDynamic.Length; // compila sin cast; se resuelve en tiempo de ejecucionobject |
dynamic |
|
|---|---|---|
| Comprobación de tipos | En tiempo de compilación (exige cast explícito para usar miembros específicos) | Pospuesta a tiempo de ejecución |
| Acceso a miembros del tipo real | Requiere is/as/cast primero (Módulo 3) |
Directo, sin cast, asumiendo que existirá |
| Error si el miembro no existe | En tiempo de compilación (no compila) | En tiempo de ejecución (RuntimeBinderException) |
| Autocompletado del IDE | Completo, sobre el tipo tras el cast | Ninguno: el IDE no puede saber qué miembros tendrá |
| Rendimiento | El de una llamada normal | Más lento: cada operación se resuelve dinámicamente cada vez |
En la práctica, object obliga a ser explícito sobre qué tipo se espera realmente (con
is/as, como en el Polimorfismo del Módulo 3) antes de operar con el valor; dynamic permite
saltarse ese paso, a cambio de perder toda garantía de que el miembro accedido existirá de
verdad cuando el programa se ejecute.
dynamic frente a var: dinámico no es lo mismo que inferido
dynamic frente a var: dinámico no es lo mismo que inferidoEs un error de principiante muy común confundir dynamic con var (Módulo 1), porque ambos
permiten omitir el nombre del tipo al declarar la variable. La diferencia es fundamental:
var titulo = "Rayuela"; // el compilador INFIERE que titulo es string, y lo fija para siempre
// titulo = 42; // Error de COMPILACION: no se puede asignar un int a una variable string
dynamic valorDinamico = "Rayuela"; // el compilador NO fija ningun tipo concreto
valorDinamico = 42; // compila perfectamente: dynamic puede cambiar de tipo en cualquier momentovar es azúcar sintáctico puro: el compilador deduce el tipo exacto a partir del valor
inicial (aquí, string) y, a partir de ese momento, titulo se comporta exactamente como si se
hubiera escrito string titulo = "Rayuela"; —con todas las comprobaciones de tipos habituales.
dynamic, en cambio, es un tipo real del sistema de tipos que renuncia a esas comprobaciones:
una variable dynamic puede contener un string en una línea y un int en la siguiente, sin
que el compilador se queje en ningún momento.
var |
dynamic |
|
|---|---|---|
| Qué hace el compilador | Infiere el tipo real una única vez, en la declaración | No fija ningún tipo; pospone todo a tiempo de ejecución |
| ¿Puede cambiar de tipo la variable? | No: el tipo inferido queda fijo | Sí: puede contener valores de tipos distintos en momentos distintos |
| Comprobación de tipos | Completa, en tiempo de compilación (como cualquier tipo explícito) | Ninguna en tiempo de compilación |
| Coste en tiempo de ejecución | Ninguno (es solo una declaración más corta) | El de resolver cada operación dinámicamente |
ExpandoObject: objetos dinámicos sin una clase definida
ExpandoObject: objetos dinámicos sin una clase definidaExpandoObject (de System.Dynamic) lleva la idea de dynamic un paso más allá: permite crear
un objeto sin ninguna clase que lo defina de antemano, añadiéndole propiedades sobre la
marcha, en tiempo de ejecución:
using System.Dynamic;
dynamic libroInformal = new ExpandoObject();
libroInformal.Titulo = "Rayuela";
libroInformal.Autor = "Julio Cortazar";
libroInformal.PaginasLeidas = 120; // ninguna clase declaro jamas esta propiedad
Console.WriteLine($"{libroInformal.Titulo}, pagina {libroInformal.PaginasLeidas}");Ninguna clase de BiblioTech define PaginasLeidas: ExpandoObject permite añadir esa
propiedad sobre la marcha porque, por debajo, no es más que un diccionario de nombre-valor
disfrazado de objeto con sintaxis de punto. Es útil en escenarios de scripting o prototipado
muy rápido, donde definir una clase completa para una estructura de datos que se usa una sola
vez resultaría desproporcionado; para cualquier dato que forme parte del dominio estable de
BiblioTech, una clase normal (como Libro) sigue siendo muchísimo mejor opción, por las razones
del apartado 6.
- Cuándo tiene sentido usar
dynamic
dynamicdynamic no es un error de diseño de C#: existen escenarios reales donde renunciar a la
comprobación de tipos en tiempo de compilación es, precisamente, lo que se necesita:
| Escenario | Por qué encaja dynamic |
|---|---|
| Interoperabilidad COM (Office, automatización de Excel/Word desde .NET) | Las APIs COM no exponen tipos estáticos que el compilador de C# pueda verificar de antemano |
| JSON no tipado de estructura variable o desconocida | Cuando no merece la pena (o no es posible) definir una clase para cada forma posible de respuesta |
| Escenarios de scripting o herramientas interactivas | Prototipado rápido donde la flexibilidad importa más que la seguridad de tipos |
Código de dominio tipado (BiblioTech: Libro, Socio, Prestamo...) |
No encaja: ver apartado 6 |
- Por qué evitar
dynamic en el dominio de BiblioTech
dynamic en el dominio de BiblioTechTodo el modelo de BiblioTech —MaterialBibliotecario, Libro, Revista, Socio,
Prestamo— se ha construido, módulo a módulo, apoyándose precisamente en lo que dynamic
descarta:
- Seguridad de tipos en tiempo de compilación:
libro1.Isbnfalla de inmediato al escribir el código, con un mensaje claro, siIsbnno existiera o estuviera mal escrito; condynamic libro1, el mismo error solo aparecería como excepción al ejecutar esa línea concreta, quizás muchas ejecuciones después de escribirla. - Autocompletado y refactorización asistida por el IDE: el editor conoce, en todo momento,
los miembros exactos de
Librogracias al tipado estático; sobre una variabledynamic, el IDE no puede ofrecer ninguna ayuda, porque no sabe qué tipo tendrá hasta que el programa se ejecute. - Rendimiento: cada operación sobre
dynamicse resuelve dinámicamente en tiempo de ejecución (con una infraestructura de resolución y una caché interna del DLR), más lenta que la llamada directa y ya resuelta de un tipo estático comoLibro.
Sustituir, por ejemplo, Libro libro1 = ... por dynamic libro1 = ... en cualquier parte de
BiblioTech no aportaría ninguna ventaja (el tipo real siempre se conoce de antemano) y sí una
pérdida clara: errores que hoy detiene el compilador pasarían a ser excepciones en tiempo de
ejecución, y el editor dejaría de ayudar con autocompletado. La regla general: usa dynamic
solo cuando el tipo realmente no se conoce de antemano y no hay forma razonable de
modelarlo con una clase; en cualquier otro caso, el tipado estático de C# es la opción correcta.
- Ejemplo: leer un JSON de estructura desconocida con
dynamic, frente al enfoque tipado
dynamic, frente al enfoque tipadoLa lección de JSON y APIs REST (Módulo 5) definió MetadatosLibroExterno y Editorial para
deserializar de forma tipada la respuesta de un servicio externo de metadatos de libros,
cuya estructura se conocía de antemano. Pero no todas las APIs externas documentan su respuesta
con esa claridad; a veces solo se dispone de un JSON de ejemplo, de estructura variable, sin
ninguna clase ya escrita para él. dynamic, combinado con el tipo dynamic que devuelve
JsonSerializer.Deserialize<dynamic> sobre un JsonElement, permite explorarlo sin definir
clases por adelantado:
using System.Text.Json;
string jsonDesconocido =
"""
{
"isbn": "978-84-376-0495-4",
"editorial": { "nombre": "Sudamericana", "pais": "Argentina" },
"generos": ["Novela", "Literatura latinoamericana"]
}
""";
// Enfoque con dynamic: sin ninguna clase definida de antemano
JsonElement raiz = JsonSerializer.Deserialize<JsonElement>(jsonDesconocido);
dynamic documento = raiz;
Console.WriteLine(documento.GetProperty("isbn").GetString()); // "978-84-376-0495-4"
Console.WriteLine(documento.GetProperty("editorial").GetProperty("pais").GetString()); // "Argentina"// Enfoque tipado equivalente, como en la leccion de JSON y APIs REST (Modulo 5)
MetadatosLibroExterno? metadatos =
JsonSerializer.Deserialize<MetadatosLibroExterno>(jsonDesconocido,
new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
Console.WriteLine(metadatos?.Isbn); // "978-84-376-0495-4"
Console.WriteLine(metadatos?.Editorial.Pais); // "Argentina"Ambos fragmentos leen el mismo dato, pero con garantías muy distintas: documento.GetProperty("isbn")
no comprueba en tiempo de compilación que la clave "isbn" exista en el JSON —si no existiera,
o si se escribiera mal ("ivbn"), el error solo aparecería como excepción al ejecutar esa
línea—; metadatos?.Isbn, en cambio, es una propiedad real de MetadatosLibroExterno, verificada
por el compilador, con autocompletado en el IDE, y con un valor null seguro y explícito si la
deserialización no encuentra los datos esperados. dynamic puede ser razonable para una
exploración rápida y puntual de un JSON de estructura todavía no conocida; en cuanto la
estructura se estabiliza (como ya ocurrió con MetadatosLibroExterno), migrar a clases tipadas
es casi siempre la decisión correcta para el código que va a mantenerse en el tiempo.
flowchart LR
A["JSON de estructura desconocida"] --> B{"Se conoce la forma exacta?"}
B -->|No, exploracion puntual| C["dynamic / JsonElement.GetProperty"]
B -->|Si, o se puede modelar| D["Clases tipadas + JsonSerializer.Deserialize-T-"]
D --> E["Seguridad de tipos, autocompletado, mejor rendimiento"]
Errores Comunes y Consejos
- Confundir
dynamicconvar:vares inferencia de tipos en tiempo de compilación (el tipo queda fijo);dynamicrenuncia a la comprobación de tipos por completo, y una misma variable puede cambiar de tipo en tiempo de ejecución. - Usar
dynamicpara evitar escribir una clase: modelar una estructura de datos estable con una clase (comoMetadatosLibroExterno) casi siempre compensa el esfuerzo inicial, a cambio de seguridad de tipos y autocompletado durante el resto de la vida del código. - Propagar
dynamicpor el código de dominio: un solodynamicen una firma de método obliga a que todo el código que lo usa después trabaje también sin comprobación de tipos; conviene aislardynamicen el borde del sistema (donde de verdad se necesita, como interoperabilidad COM) y convertir a tipos concretos cuanto antes. - No probar el camino de error de
dynamic: como el compilador no avisa de nada, un típo con una propiedad mal escrita (documento.GetProperty("ivbn")) solo se descubre ejecutando esa línea exacta; sin pruebas que cubran ese camino, el error puede pasar desapercibido hasta producción. - Consejo: si te sorprendes escribiendo
dynamicen el código de dominio de una aplicación como BiblioTech, es una señal de que probablemente falta una clase por definir, no una razón para seguir adelante condynamic.
Ejercicios
-
Declara una variable
dynamic valorcon el texto"BiblioTech", imprimevalor.Length, y después reasígnale el número2026. Explica (en un comentario) por qué esa reasignación compila sin error, a diferencia de intentar lo mismo con una variablevar. -
Usando
JsonSerializer.Deserialize<JsonElement>y una variabledynamic, extrae el primer elemento del array"generos"del JSON de ejemplo del apartado 7 (pista:documento.GetProperty("generos")[0].GetString()). -
Explica, en un párrafo breve, por qué convertir
Libroendynamic Libro(es decir, declarardynamic libro1 = new Libro(...)en vez deLibro libro1 = new Libro(...)) en BiblioTech sería una mala decisión de diseño, citando al menos dos de las razones vistas en el apartado 6.
Soluciones
dynamic valor = "BiblioTech";
Console.WriteLine(valor.Length); // 10
valor = 2026; // Compila: dynamic no fija ningun tipo concreto, puede contener cualquier valor
// Con "var valor = "BiblioTech";", "valor = 2026;" seria un error de COMPILACION,
// porque var infiere string una unica vez y el tipo queda fijo para siempre.
using System.Text.Json;
string jsonDesconocido =
"""
{
"isbn": "978-84-376-0495-4",
"editorial": { "nombre": "Sudamericana", "pais": "Argentina" },
"generos": ["Novela", "Literatura latinoamericana"]
}
""";
JsonElement raiz = JsonSerializer.Deserialize<JsonElement>(jsonDesconocido);
dynamic documento = raiz;
Console.WriteLine(documento.GetProperty("generos")[0].GetString()); // "Novela"
Convertir Libro en dynamic eliminaría la comprobación en tiempo de compilación de todo el
código que use libro1 (un error como libro1.Isbnn —con errata— pasaría de un error de
compilación inmediato a una excepción en tiempo de ejecución, posiblemente en producción); y
el IDE dejaría de poder ofrecer autocompletado sobre sus propiedades y métodos, ya que no
podría saber de antemano qué miembros tendrá libro1. Además, cada acceso a sus miembros
sería más lento, al resolverse dinámicamente en cada llamada en vez de estar ya resuelto por
el compilador.
Conclusión
En esta lección has visto el tipo dynamic: cómo pospone toda comprobación de tipos a tiempo de
ejecución, en qué se diferencia de object (que exige cast explícito) y de var (que infiere
un tipo fijo en tiempo de compilación), qué es ExpandoObject, y en qué escenarios reales
(interoperabilidad COM, JSON no tipado, scripting) tiene sentido usarlo. También has confirmado
por qué el dominio tipado de BiblioTech debe seguir apoyándose en clases concretas como Libro
en vez de dynamic, y visto un ejemplo de exploración de JSON no tipado frente al enfoque tipado
ya usado en el Módulo 5.
La siguiente lección, Gestión de Memoria y Recolección de Basura, deja atrás el sistema de tipos
para entrar en cómo .NET gestiona la memoria de cualquier objeto —tipado o dinámico por
igual—: qué diferencia hay entre la pila (stack) y el montón (heap), cómo decide el
recolector de basura cuándo liberar un objeto, y cómo liberar explícitamente recursos externos
(ficheros, conexiones) con IDisposable, retomando el patrón using ya visto en el Módulo 5.
Curso de Programación en C#
Módulo 1: Introducción a C#
- Introducción a C#
- Configuración del Entorno de Desarrollo
- Programa Hola Mundo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Arrays y Cadenas de Texto
Módulo 2: Estructuras de Control
Módulo 3: Programación Orientada a Objetos
- Clases y Objetos
- Métodos
- Constructores y Destructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- Structs y Records: Tipos por Valor y por Referencia
Módulo 4: Conceptos Avanzados de C#
- Interfaces
- Delegados y Eventos
- Pattern Matching y Características Modernas de C#
- Genéricos
- Colecciones
- LINQ (Consulta Integrada en el Lenguaje)
- Programación Asíncrona
Módulo 5: Trabajando con Datos
- Entrada/Salida de Archivos
- Serialización
- Conectividad con Bases de Datos
- Entity Framework
- Trabajo con JSON y Consumo de APIs REST
Módulo 6: Temas Avanzados
- Reflexión
- Atributos
- Programación Dinámica
- Gestión de Memoria y Recolección de Basura
- Multihilo y Programación Paralela
Módulo 7: Construcción de Aplicaciones
- Formularios de Windows
- WPF (Windows Presentation Foundation)
- ASP.NET Core
- Blazor
- Xamarin y .NET MAUI
Módulo 8: Mejores Prácticas y Patrones de Diseño
- Estándares de Codificación y Mejores Prácticas
- Patrones de Diseño
- Inyección de Dependencias e Inversión de Control
- Pruebas Unitarias
- Revisión y Refactorización de Código
