En la lección de Polimorfismo (Módulo 3) ya usaste is Libro libro para comprobar el tipo real
de un MaterialBibliotecario en tiempo de ejecución, y en Sentencias Switch (Módulo 2) viste
un primer adelanto de las switch expressions. Ambas ideas forman parte de una característica
más amplia de C# moderno: el pattern matching, que permite comprobar no solo el tipo de un
valor, sino también sus propiedades, sus rangos y combinaciones lógicas entre condiciones, todo
en una sintaxis compacta. Esta lección reúne el pattern matching, las switch expressions, los
nullable reference types y un recordatorio de los records, como el conjunto de
características que definen el estilo del C# actual, y las aplica para clasificar y filtrar
materiales de BiblioTech.
Contenido
- Recordatorio: patrones de tipo con
is - Patrones de propiedad:
{ Disponible: true } - Patrones relacionales y patrones lógicos:
and,or,not,>,< - Switch expressions: la evolución del
switchclásico - Patrones en switch expressions: combinando tipo y propiedad
- Nullable reference types:
#nullable enable,?y el operador! - Records como parte del C# moderno: recordatorio y patrón posicional
- Recordatorio: patrones de tipo con
is
isEn la lección de Polimorfismo ya escribiste código como este, usando un patrón de tipo (type pattern):
MaterialBibliotecario material = new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
if (material is Libro libro)
{
Console.WriteLine($"Es un libro con ISBN {libro.Isbn}");
}material is Libro libro comprueba, en tiempo de ejecución, si material es (o hereda de)
Libro, y si lo es, declara automáticamente una nueva variable libro de tipo Libro, ya
convertida, disponible dentro del bloque. Esto ya es pattern matching: un patrón de tipo es el
más simple de todos los patrones que ofrece C#, y a partir de aquí la lección presenta patrones
más expresivos que se combinan con él.
- Patrones de propiedad:
{ Disponible: true }
{ Disponible: true }Un patrón de propiedad (property pattern) comprueba el valor de una o más propiedades de un objeto, sin necesidad de una variable intermedia:
if (material is { Disponible: true })
{
Console.WriteLine("El material esta disponible para prestamo.");
}material is { Disponible: true } se lee como "si material tiene una propiedad Disponible
igual a true". Este patrón se puede combinar con un patrón de tipo, comprobando a la vez
el tipo concreto y una de sus propiedades:
if (material is Libro { Disponible: true } libro)
{
Console.WriteLine($"'{libro.Titulo}' es un libro disponible.");
}Aquí se comprueban, en una sola expresión, tres cosas a la vez: que material sea (en tiempo
de ejecución) un Libro, que su Disponible sea true, y además se obtiene la variable
libro ya convertida, todo en una línea que antes habría requerido un if anidado.
- Patrones relacionales y patrones lógicos:
and, or, not, >, <
and, or, not, >, <Los patrones relacionales comparan un valor numérico usando los operadores <, >, <=,
>= directamente dentro de un patrón:
Revista revista1 = new Revista("National Geographic", "Varios autores", 302);
if (revista1 is Revista { NumeroEdicion: > 100 })
{
Console.WriteLine("Revista de larga trayectoria (mas de 100 ediciones).");
}Los patrones lógicos combinan varios patrones con las palabras clave and, or y not
(en lugar de &&, ||, !, reservados para expresiones booleanas normales):
bool esRevistaConsolidada = revista1 is Revista { NumeroEdicion: > 50 and < 1000 };
bool esDisponibleYNoRevista = material is not Revista and { Disponible: true };| Patrón | Ejemplo | Significado |
|---|---|---|
| De tipo | material is Libro |
¿Es (en tiempo de ejecución) un Libro? |
| De tipo con variable | material is Libro libro |
Igual que el anterior, y además asigna libro |
| De propiedad | material is { Disponible: true } |
¿La propiedad Disponible vale true? |
| Relacional | numeroEdicion is > 100 |
¿El valor es mayor que 100? |
Lógico and/or/not |
material is not Revista and { Disponible: true } |
Combina varios patrones con lógica |
| Combinado (tipo + propiedad) | material is Libro { Disponible: true } libro |
Tipo, propiedad y variable a la vez |
Estos patrones no están limitados a if: se pueden usar en cualquier expresión is, y —lo
más habitual en código moderno— dentro de una switch expression, que es el tema del siguiente
apartado.
- Switch expressions: la evolución del
switch clásico
switch clásicoEn la lección de Sentencias Switch (Módulo 2) viste un primer adelanto de las switch
expressions: una forma de escribir un switch que devuelve un valor directamente, sin
case/break repetidos, usando => para cada posible resultado:
string categoria = "Novela";
string seccion = categoria switch
{
"Novela" => "Narrativa - Planta 1",
"Ensayo" => "No ficcion - Planta 2",
"Poesia" => "Poesia - Planta 1",
_ => "Categoria no reconocida"
};switch clásico (sentencia) |
Switch expression | |
|---|---|---|
| ¿Devuelve un valor directamente? | No; hay que asignar dentro de cada case |
Sí; toda la expresión se evalúa a un valor |
| Palabra clave por caso | case valor: ... break; |
patron => valor, |
| Caso por defecto | default: |
_ (guion bajo) |
| Verbosidad | Mayor (llaves, break en cada caso) |
Menor (una línea por caso, en general) |
| Compatible con patrones (tipo, propiedad, relacional...) | Limitado | Completo |
La diferencia más importante para esta lección es la última fila: mientras que el switch
clásico solo compara igualdad exacta contra valores constantes, una switch expression admite
cualquier patrón de los vistos en los apartados 1 a 3, lo que la convierte en la
herramienta natural para clasificar objetos como los de BiblioTech según su tipo real y sus
propiedades.
- Patrones en switch expressions: combinando tipo y propiedad
Así se clasifica un MaterialBibliotecario combinando patrones de tipo y de propiedad dentro
de una switch expression:
string Clasificar(MaterialBibliotecario material) => material switch
{
Libro { Disponible: true } => $"Libro disponible: {material.Titulo}",
Libro => $"Libro prestado: {material.Titulo}",
Revista { NumeroEdicion: > 100 } => $"Revista de larga trayectoria: {material.Titulo}",
Revista => $"Revista: {material.Titulo}",
_ => "Tipo de material no reconocido"
};Libro libro1 = new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Revista revista1 = new Revista("National Geographic", "Varios autores", 302);
libro1.Prestar();
Console.WriteLine(Clasificar(libro1)); // "Libro prestado: Rayuela"
Console.WriteLine(Clasificar(revista1)); // "Revista de larga trayectoria: National Geographic"El orden de los casos importa, igual que en un switch clásico: C# evalúa cada patrón de
arriba abajo y ejecuta el primero que coincida. Por eso Libro { Disponible: true } debe ir
antes que Libro a secas: si Libro fuera el primer caso, siempre coincidiría antes de
comprobar Disponible, y el caso más específico nunca se alcanzaría.
Filtrar una lista completa del catálogo reutiliza exactamente el mismo patrón, esta vez dentro de una expresión booleana con LINQ (que se estudiará en detalle en una lección posterior de este módulo):
List<MaterialBibliotecario> catalogo = new List<MaterialBibliotecario> { libro1, revista1 };
foreach (MaterialBibliotecario material in catalogo)
{
if (material is Libro and { Disponible: true })
{
Console.WriteLine($"Disponible para prestamo: {material.Titulo}");
}
}
- Nullable reference types:
#nullable enable, ? y el operador !
#nullable enable, ? y el operador !Ya usaste Libro? en la lección de Polimorfismo (Libro? libroCasteado = primerElemento as Libro;) sin detenerte en el porqué del signo ?. C# moderno permite activar los nullable
reference types, una característica que hace que el compilador distinga, para los tipos por
referencia (class, no solo string/int?), entre "nunca puede ser null" y "puede ser
null", avisando con advertencias cuando el código no respeta esa distinción.
Se activa (muchos proyectos nuevos de .NET lo traen activado por defecto) con esta directiva al principio del fichero:
Con esta directiva activa, un tipo como Libro significa "nunca null" salvo que se declare
explícitamente Libro?:
Libro? BuscarLibroPorIsbn(List<Libro> libros, string isbn)
{
foreach (Libro libro in libros)
{
if (libro.Isbn == isbn)
{
return libro;
}
}
return null; // valido: el tipo de retorno es Libro?, admite null
}Libro? resultado = BuscarLibroPorIsbn(catalogoLibros, "978-84-376-0495-4");
if (resultado is not null)
{
Console.WriteLine(resultado.Titulo); // dentro del "if", el compilador sabe que no es null
}El operador ! (llamado null-forgiving operator, "operador de indulgencia con null") le dice
al compilador "confía en mí, sé que este valor no es null aquí, aunque su tipo sea nullable",
silenciando la advertencia sin añadir ninguna comprobación real en tiempo de ejecución:
Libro libroSeguro = BuscarLibroPorIsbn(catalogoLibros, "978-84-376-0495-4")!;
// Advertencia silenciada, pero si el resultado fuera realmente null, esto lanzaria
// NullReferenceException en tiempo de ejecucion al usar libroSeguro mas adelante.| Herramienta | Qué expresa |
|---|---|
Libro (sin ?) |
El compilador espera que nunca sea null; avisa si detecta lo contrario |
Libro? |
Puede ser null; el compilador exige comprobarlo antes de usarlo con confianza |
is not null / is null |
Patrón recomendado para comprobar nulidad (más claro que != null) |
valor! |
"Confío en que no es null aquí", sin comprobación real; úsalo con moderación |
Es importante entender que los nullable reference types son, sobre todo, una ayuda del
compilador en tiempo de compilación (avisos, no errores duros en la mayoría de los casos):
no impiden que un null real llegue en tiempo de ejecución si el operador ! se usa de forma
incorrecta, pero ayudan enormemente a detectar posibles NullReferenceException antes de
ejecutar el programa.
- Records como parte del C# moderno: recordatorio y patrón posicional
La última lección del Módulo 3 presentó los record, con ResumenPrestamo como ejemplo:
Los records forman parte, junto con el pattern matching, las switch expressions y los nullable reference types, del conjunto de características que definen el C# moderno: todas comparten el objetivo de expresar más significado con menos código, y de detectar más errores en tiempo de compilación. Una conexión directa entre records y pattern matching es el patrón posicional, que deconstruye un record directamente en variables, usando el mismo orden que su constructor:
ResumenPrestamo resumen = new ResumenPrestamo("Rayuela", "Ana Martinez", new DateTime(2026, 1, 10));
if (resumen is ResumenPrestamo(var tituloLibro, var nombreSocio, _))
{
Console.WriteLine($"{nombreSocio} tiene en prestamo '{tituloLibro}'");
}ResumenPrestamo(var tituloLibro, var nombreSocio, _) extrae las tres propiedades posicionales
del record en dos variables nuevas, descartando la tercera con _ (el mismo guion bajo que ya
conoces como caso "por defecto" de una switch expression, aquí usado como "no me interesa este
valor"). Esta capacidad de deconstrucción automática es otra ventaja que los records ofrecen
"gratis", sin escribir código adicional, gracias a su sintaxis posicional.
Errores Comunes y Consejos
- Olvidar el orden en una switch expression con patrones de tipo: igual que en un
switchclásico concaseapilados, un caso más general antes que uno más específico "tapa" al segundo; coloca siempre los patrones más restrictivos (Libro { Disponible: true }) antes que los más generales (Libro). - Confundir
and/or/notde los patrones con&&/||/!de las expresiones booleanas: son sintaxis distintas para contextos distintos; dentro de un patrón (is ..., o dentro de uncasede switch expression) se usan las palabrasand/or/not, no los operadores simbólicos. - Activar
#nullable enablea mitad de proyecto y esperar cero advertencias: al activarlo sobre código ya existente es normal (y esperable) que aparezcan avisos en sitios que necesitan revisión; no ignores esos avisos sistemáticamente, revísalos uno a uno. - Abusar del operador
!: usarlo para silenciar cualquier aviso sin analizar si el valor puede ser realmentenullreintroduce, por la puerta de atrás, el mismo riesgo deNullReferenceExceptionque los nullable reference types están pensados para prevenir. - Consejo: prefiere
is not nulla!= null(yis nulla== null) en código moderno; además de ser más legible,isno se puede sobrescribir de forma sorprendente como sí puede ocurrir, en tipos concretos, con el operador==.
Ejercicios
-
Escribe un método
string Clasificar(MaterialBibliotecario material)que use una switch expression con patrones de tipo y de propiedad para devolver:"Libro disponible"si es unLibroconDisponible: true,"Libro prestado"si es unLibrono disponible,"Revista"en cualquier otro caso deRevista, y"Desconocido"para cualquier otro tipo. Pruébalo con unLibrodisponible, uno prestado y unaRevista. -
Activa
#nullable enableal principio de un fichero de prueba. Escribe un métodoLibro? BuscarPorTitulo(List<Libro> libros, string titulo)que devuelva el primer libro cuyoTitulocoincida, onullsi no lo encuentra. Llama al método dos veces (un título que exista y otro que no) y, en cada caso, comprueba conis not nullantes de acceder a sus propiedades. -
Dado un
record ResumenPrestamo(string TituloLibro, string NombreSocio, DateTime FechaPrestamo), crea un objeto de ejemplo y usa un patrón posicional (is ResumenPrestamo(var titulo, var nombre, var fecha)) para extraer sus tres valores en variables nuevas y mostrarlos por separado.
Soluciones
string Clasificar(MaterialBibliotecario material) => material switch
{
Libro { Disponible: true } => "Libro disponible",
Libro => "Libro prestado",
Revista => "Revista",
_ => "Desconocido"
};
Libro libroDisponible = new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Libro libroPrestado = new Libro("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1");
libroPrestado.Prestar();
Revista revista1 = new Revista("National Geographic", "Varios autores", 302);
Console.WriteLine(Clasificar(libroDisponible)); // "Libro disponible"
Console.WriteLine(Clasificar(libroPrestado)); // "Libro prestado"
Console.WriteLine(Clasificar(revista1)); // "Revista"
#nullable enable
Libro? BuscarPorTitulo(List<Libro> libros, string titulo)
{
foreach (Libro libro in libros)
{
if (libro.Titulo == titulo)
{
return libro;
}
}
return null;
}
List<Libro> libros = new List<Libro>
{
new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4")
};
Libro? encontrado = BuscarPorTitulo(libros, "Rayuela");
if (encontrado is not null)
{
Console.WriteLine(encontrado.Isbn); // Ok, dentro del "if" ya se sabe que no es null
}
Libro? noEncontrado = BuscarPorTitulo(libros, "Otro titulo");
if (noEncontrado is null)
{
Console.WriteLine("No se encontro el libro.");
}
record ResumenPrestamo(string TituloLibro, string NombreSocio, DateTime FechaPrestamo);
ResumenPrestamo resumen = new ResumenPrestamo("Rayuela", "Ana Martinez", new DateTime(2026, 1, 10));
if (resumen is ResumenPrestamo(var titulo, var nombre, var fecha))
{
Console.WriteLine($"Titulo: {titulo}");
Console.WriteLine($"Socio: {nombre}");
Console.WriteLine($"Fecha: {fecha:yyyy-MM-dd}");
}
Conclusión
En esta lección has ampliado el is y el switch que ya conocías con patrones de tipo, de
propiedad, relacionales y lógicos; has aprendido a usar switch expressions con esos mismos
patrones para clasificar objetos de forma compacta; has activado los nullable reference types
para que el compilador te ayude a prevenir referencias nulas; y has visto los records, ya
conocidos del Módulo 3, deconstruirse con un patrón posicional. Todo esto forma, en conjunto,
el estilo idiomático del C# actual.
Hasta ahora, cada ejemplo de BiblioTech ha trabajado con objetos individuales o, como mucho,
con un List<MaterialBibliotecario> improvisado dentro de un método. La próxima lección da un
paso atrás para introducir los genéricos: la característica que hace posible que List<T>
funcione con cualquier tipo, y que te permitirá escribir tus propias clases y métodos
reutilizables para cualquier tipo de dato, antes de construir, en la lección de Colecciones, la
clase Biblioteca completa.
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
