C# es el lenguaje de Microsoft para la plataforma .NET: con él se hacen servidores web, aplicaciones de escritorio, herramientas de línea de órdenes y la mayoría de los videojuegos hechos con Unity. Es un lenguaje con tipos, compilado y de código abierto, que funciona igual en Windows, Linux y macOS. Este curso empieza desde cero, aunque si ya programas en otro lenguaje irás más rápido. Todo el código está comprobado con .NET 10 y C# 14, las versiones actuales.
La ruta: ocho lecciones
Están pensadas para leerse en orden. Las dos primeras están en esta misma página y las demás tienen la suya. Cada lección tiene un recuadro «El error que te ahorra» con un fallo que el compilador detiene antes de que el programa llegue a ejecutarse, al menos un esquema y retos al final con la solución escondida. Al terminar hay un test de 24 preguntas con un formato distinto: adivinar si el código compila.
- 1Primeros pasosQué es C# y .NET, dotnet run con un solo archivo y el primer proyecto.En esta página
- 2Tipos y nullValor y referencia, números, texto y los avisos de null del compilador.En esta página
- 3Control y coleccionesif, switch con patrones, bucles, List, Dictionary y rangos.Leer la lección →
- 4Métodos y excepcionesParámetros, lambdas, try/catch, using y TryParse.Leer la lección →
- 5Clases y recordsPropiedades, constructores, records con igualdad por valor y struct.Leer la lección →
- 6Interfaces y genéricosHerencia, interfaces, genéricos con where e iteradores con yield.Leer la lección →
- 7LINQ y asyncConsultas sobre colecciones y tareas que esperan sin bloquear.Leer la lección →
- 8Un proyecto completoUna herramienta con .csproj, CSV, JSON, pruebas y publicación.Leer la lección →
Lección 1. Primeros pasos
Qué es C# y qué es .NET
C# (se pronuncia «ce sharp») es el lenguaje: la sintaxis, las palabras clave, las reglas de los tipos. Lo diseñó Anders Hejlsberg en Microsoft y la primera versión salió en 2002. .NET es la plataforma sobre la que se ejecuta: el runtime que corre los programas y una biblioteca enorme con todo lo habitual, desde leer archivos hasta hablar HTTP. Durante años solo funcionó en Windows; desde 2016, con .NET Core, es de código abierto y multiplataforma, y hoy se llama simplemente .NET.
Cada noviembre sale una versión nueva de .NET, y las pares tienen soporte largo (LTS) de tres años. .NET 10, de noviembre de 2025, es la LTS actual y trae C# 14. Es la que usa este curso.
El esquema explica dos cosas que verás en todo el curso. La primera: los errores de tipos los encuentra el compilador, antes de ejecutar nada, como en Java o TypeScript y al contrario que en Python o JavaScript. La segunda: el resultado no es un ejecutable de una plataforma concreta, sino un .dll con código intermedio que el runtime de .NET ejecuta en cualquier sistema.
Instalar .NET
Descarga el SDK de .NET 10 desde dotnet.microsoft.com/download; el SDK incluye el compilador, el runtime y la herramienta dotnet. En Linux también está en los repositorios de Ubuntu y Fedora, y en macOS con Homebrew. Para comprobar que está bien instalado:
dotnet --version
salida10.0.403
Para escribir código vale cualquier editor. Los más usados son Visual Studio Code con la extensión C# Dev Kit, Visual Studio en Windows y JetBrains Rider.
El primer programa, en un solo archivo
Crea un archivo hola.cs con una sola línea:
Console.WriteLine("Hola, mundo");Y ejecútalo:
dotnet run hola.cs
salidaHola, mundo
Desde .NET 10, dotnet run acepta un archivo suelto, sin proyecto: lo compila y lo ejecuta. La primera vez tarda unos segundos porque compila; las siguientes, si no has cambiado nada, arranca enseguida. Es la forma más cómoda de probar las cosas de este curso, y la usaremos hasta la lección 8.
Si has visto C# antiguo te faltará algo: la clase Program y el método Main. Siguen existiendo, pero desde C# 9 el compilador los escribe por ti cuando el archivo empieza directamente con instrucciones. Se llaman instrucciones de nivel superior. Este programa es equivalente y también compila:
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hola, mundo");
}
}Y Console, que vive en el espacio de nombres System, se puede usar sin escribir using System; porque .NET añade solo los using más habituales (los using implícitos).
Variables, texto y la consola
var nombre = "Ana";
var edad = 31;
Console.WriteLine($"Hola, {nombre}. El año que viene tendrás {edad + 1} años.");
Console.Write("¿Cómo te llamas? ");
string? respuesta = Console.ReadLine();
Console.WriteLine($"Encantado, {respuesta}.");salidaHola, Ana. El año que viene tendrás 32 años. ¿Cómo te llamas? Encantado, Luis.
Tres cosas nuevas. var declara una variable y deja que el compilador deduzca su tipo del valor: nombre es un string y edad un int, y lo serán siempre. El $ delante de las comillas activa la interpolación: lo que va entre llaves se evalúa y se mete en el texto. Y Console.ReadLine() lee una línea de lo que escribe el usuario (en la salida no aparece «Luis» porque aquí la entrada vino de un archivo y no del teclado). El ? de string? lo explicamos en la lección 2: es la forma de decir que puede no llegar nada.
Tu primer error
Toda instrucción termina en punto y coma. Si se te olvida, el programa ni siquiera llega a ejecutarse:
Console.WriteLine("Hola, mundo")dotnet buildpcoma.cs(1,33): error CS1002: Se esperaba ;
El mensaje dice el archivo, la línea y la columna entre paréntesis, si es un error o un aviso, su código (CS1002, útil para buscarlo) y la explicación. Si tu sistema está en español, los mensajes del compilador salen en español, como aquí.
El error que te ahorra: pasar un dato del tipo equivocado
string Saludar(string nombre)
{
return $"Hola, {nombre}";
}
Console.WriteLine(Saludar("Ana"));
Console.WriteLine(Saludar(42));dotnet buildhola_mal.cs(7,27): error CS1503: Argumento 1: no se puede convertir de 'int' a 'string'
La función Saludar pide un string y le pasamos un número. En JavaScript o Python el programa arrancaría y fallaría (o haría algo raro) al llegar a esa línea, quizá meses después. En C# no se ejecuta ni la primera línea hasta que lo arreglas.
Un proyecto de verdad
Un archivo suelto es perfecto para aprender, pero las aplicaciones tienen varios archivos, dependencias y configuración. Para eso está el proyecto:
dotnet new console -o MiApp cd MiApp dotnet run
salidaLa plantilla "Aplicación de consola" se creó correctamente. Procesando acciones posteriores a la creación... Restaurando /tmp/t/MiApp/MiApp.csproj: Determinando los proyectos que se van a restaurar... Se ha restaurado /tmp/t/MiApp/MiApp.csproj (en 61 ms). Restauración realizada correctamente. Hello, World!
dotnet new console crea una carpeta con dos archivos: Program.cs, con el «Hello, World!» de siempre, y MiApp.csproj, que describe el proyecto:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>OutputType dice que es un ejecutable; TargetFramework, la versión de .NET; ImplicitUsings activa los using automáticos, y Nullable activa los avisos de null de la lección 2. Al compilar con dotnet build, el resultado queda en bin/Debug/net10.0/:
bin/Debug/net10.0MiApp MiApp.deps.json MiApp.dll MiApp.pdb MiApp.runtimeconfig.json
Ahí está el MiApp.dll del esquema de arriba y, en Linux y macOS, un lanzador MiApp que lo arranca. Para entregarlo a otra persona se usa dotnet publish, que veremos en la lección 8.
Lección 2. Tipos y null
Cada valor tiene un tipo, y no cambia
En C# toda variable tiene un tipo fijo, lo escribas o lo deduzca el compilador con var. Estos son los que más usarás:
| Tipo | Qué guarda | Ejemplo |
|---|---|---|
int | enteros de 32 bits, hasta unos 2,100 millones | int edad = 31; |
long | enteros de 64 bits | long poblacion = 8_100_000_000; |
double | decimales en coma flotante, rápidos pero aproximados | double pi = 3.14159; |
decimal | decimales exactos, para dinero | decimal precio = 19.99m; |
bool | true o false | bool activo = true; |
char | un carácter, entre comillas simples | char inicial = 'A'; |
string | texto, entre comillas dobles | string nombre = "Ana"; |
Con var el tipo se deduce, pero sigue estando: no es una variable «sin tipo» como en JavaScript. Por eso var exige un valor inicial, porque sin él no hay de dónde deducirlo:
var x; x = 5;
dotnet buildvar_sin.cs(1,5): error CS0818: Las variables con tipo implícito se deben inicializar
Los números tienen sorpresas
Console.WriteLine(7 / 2); Console.WriteLine(7 / 2.0); Console.WriteLine(0.1 + 0.2); Console.WriteLine(0.1m + 0.2m); int maximo = int.MaxValue; Console.WriteLine(maximo + 1); Console.WriteLine((int)3.9);
salida3 3.5 0.30000000000000004 0.3 -2147483648 3
Línea a línea: dividir dos enteros da un entero, sin decimales (7 / 2 es 3); basta que uno sea decimal para que el resultado lo sea. double no puede representar exactamente 0.1, y se nota en la tercera línea; decimal, con su sufijo m, sí, por eso es el tipo para precios e importes. Un int que se pasa de su máximo da la vuelta sin avisar y se vuelve negativo. Y convertir un decimal a entero con (int) corta los decimales, no redondea.
Console.WriteLine(3.5) escribe 3,5: .NET usa el formato de números del sistema. Lo mismo pasa al revés con double.Parse("3.5"), que en un sistema en español puede leer 35. Cuando el formato importe (archivos, JSON, CSV), usa CultureInfo.InvariantCulture.Si un desbordamiento silencioso es inaceptable, por ejemplo al sumar dinero en céntimos, checked lo convierte en un error al ejecutar:
int maximo = int.MaxValue; Console.WriteLine(checked(maximo + 1));
salida con errorUnhandled exception. System.OverflowException: Arithmetic operation resulted in an overflow.
Los errores al ejecutar se llaman excepciones; los mensajes del runtime salen en inglés aunque el sistema esté en español. Cómo capturarlas está en la lección 4.
Convertir texto en números
Lo que llega del teclado, de un archivo o de la red es texto. Para convertirlo hay dos caminos: int.Parse, que lanza una excepción si el texto no es un número, y int.TryParse, que devuelve true o false y deja el número en una variable que se declara ahí mismo con out:
Console.WriteLine(int.Parse("42") + 1);
if (int.TryParse("cuarenta", out int n))
{
Console.WriteLine($"Es el número {n}");
}
else
{
Console.WriteLine("Eso no es un número");
}salida43 Eso no es un número
Con datos que escribe una persona, usa siempre TryParse: que alguien se equivoque al teclear no es una situación excepcional.
Tipos de valor y tipos de referencia
Esta es la distinción más importante de C#, y explica muchos comportamientos que al principio parecen raros. Los números, bool, char y los struct son tipos de valor: la variable contiene el dato. Las clases, los arrays y las listas son tipos de referencia: la variable contiene una referencia al objeto, que vive en otra parte de la memoria.
int a = 5;
int b = a;
b = 9;
Console.WriteLine($"a = {a}, b = {b}");
var p = new Punto { X = 5 };
var q = p;
q.X = 9;
Console.WriteLine($"p.X = {p.X}, q.X = {q.X}");
class Punto
{
public int X { get; set; }
}salidaa = 5, b = 9 p.X = 9, q.X = 9
La clase Punto va al final del archivo porque, con instrucciones de nivel superior, las declaraciones de tipos tienen que ir detrás. Las clases tienen su propia lección, la 5; aquí solo importa ver que q = p no crea otro punto. string es un caso especial: es un tipo de referencia, pero no se puede modificar (cada cambio crea un texto nuevo), así que en la práctica se comporta como un valor.
null y los avisos del compilador
Una variable de un tipo de referencia puede no apuntar a nada: vale null. Usarla entonces provoca la excepción más famosa de .NET, NullReferenceException. Para evitarla, C# distingue en los tipos si algo puede ser null: string es un texto que siempre está, y string? es un texto que puede faltar. Con Nullable activado, como en todo proyecto nuevo y en los archivos sueltos, el compilador vigila esa diferencia:
El error que te ahorra: usar algo que puede ser null
string? nombre = args.Length > 0 ? args[0] : null; Console.WriteLine(nombre.Length);
dotnet buildnulo.cs(2,19): warning CS8602: Desreferencia de una referencia posiblemente NULL.
salida con errorUnhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
Fíjate en la palabra warning: es un aviso, no un error, y el programa compila y se ejecuta igualmente. Por compatibilidad con el código antiguo, C# no impide compilar por esto. Pero el aviso señalaba exactamente la línea que luego falla: no lo ignores. Si quieres que sea un error de verdad, añade <WarningsAsErrors>nullable</WarningsAsErrors> al .csproj.
Para trabajar con valores que pueden faltar hay cuatro operadores:
string? apodo = null;
Console.WriteLine(apodo?.Length); // ?. si es null, da null en vez de fallar
Console.WriteLine(apodo?.Length ?? 0); // ?? valor por omisión si es null
apodo ??= "sin apodo"; // ??= asigna solo si es null
Console.WriteLine(apodo);
string? ciudad = Environment.GetEnvironmentVariable("CIUDAD") ?? "Lima";
Console.WriteLine(ciudad!.ToUpper()); // ! «sé que no es null»: calla el avisosalida 0 sin apodo LIMA
La primera línea escribe una línea vacía, porque apodo?.Length vale null. El ! del final no comprueba nada: solo le dice al compilador que tú sabes que no es null. Si te equivocas, el programa falla igual, así que úsalo poco.
Los tipos de valor no pueden ser null… salvo que lo pidas con ?: un int? es un entero que puede faltar, útil para datos opcionales como la edad de un formulario.
int? edad = null; Console.WriteLine(edad.HasValue); Console.WriteLine(edad ?? 18); edad = 31; Console.WriteLine(edad.Value + 1);
salidaFalse 18 32
Resumen de las lecciones 1 y 2
| Para | Escribe | Recuerda |
|---|---|---|
| Ejecutar un archivo | dotnet run hola.cs | desde .NET 10; sin proyecto |
| Crear un proyecto | dotnet new console -o MiApp | el .csproj lleva la configuración |
| Una variable | var n = 5; o int n = 5; | el tipo no cambia nunca |
| Dinero | decimal precio = 19.99m; | double es aproximado |
| Texto a número | int.TryParse(texto, out int n) | Parse lanza si falla |
| Algo que puede faltar | string?, int? | el aviso CS8602 no es un adorno |
| Un valor por omisión | x ?? "nada" | ?. evita el fallo si es null |
Retos
Reto 1. Este programa compila, pero escribe 2 en vez de 2.5. Arréglalo cambiando lo mínimo. int total = 5; int partes = 2; Console.WriteLine(total / partes);
Ver la solución
int total = 5; int partes = 2; Console.WriteLine((double)total / partes);
salida2.5
Basta con convertir uno de los dos a double antes de dividir. Ojo: (double)(total / partes) no sirve, porque convierte el resultado de una división entera que ya perdió los decimales.
Reto 2. Pide al usuario su año de nacimiento con Console.ReadLine() y escribe cuántos años cumple en 2026. Si no escribe un número, que el programa lo diga sin fallar.
Ver la solución
Console.Write("¿En qué año naciste? ");
string? texto = Console.ReadLine();
if (int.TryParse(texto, out int anio))
{
Console.WriteLine($"En 2026 cumples {2026 - anio} años.");
}
else
{
Console.WriteLine("Eso no parece un año.");
}salida¿En qué año naciste? En 2026 cumples 31 años.
TryParse acepta un string?: si llega null, simplemente devuelve false.
Reto 3. ¿Qué escribe este programa? Piénsalo antes de mirar. decimal a = 0.1m; double b = 0.1; Console.WriteLine(a * 3 == 0.3m); Console.WriteLine(b * 3 == 0.3);
Ver la solución
decimal a = 0.1m; double b = 0.1; Console.WriteLine(a * 3 == 0.3m); Console.WriteLine(b * 3 == 0.3);
salidaTrue False
Con decimal la cuenta es exacta; con double, 0.1 * 3 da 0.30000000000000004 y la comparación falla. Nunca compares double con ==: comprueba que la diferencia sea muy pequeña.
Cómo está hecho este curso
Cada fragmento de código se compiló con el SDK de .NET 10.0.403 (C# 14), con los mensajes del compilador en español, y se ejecutó con .NET 10.0.14 en un contenedor limpio. Los mensajes y las salidas están copiados de esa ejecución, con su código CS para que los puedas buscar.
¿Buscas una receta concreta en vez de una lección? En Ejemplos de C# hay tareas resueltas: peticiones HTTP, JSON, CSV, fechas, una base de datos SQLite, un servidor web mínimo… Y si vienes de Java, el curso de Java te servirá para comparar: los dos lenguajes se parecen mucho.
