Curso de C# desde cero, en español

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.

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 archivo hola.cs pasa por el compilador Roslyn, que revisa los tipos y produce hola.dll en código intermedio IL; el runtime de .NET lo convierte a código máquina al ejecutarlo con el compilador JITDel archivo .cs al programa en marchahola.cstu códigocompiladorRoslyn: revisa tiposhola.dllIL, código intermedioruntime .NETJIT: a código máquinaLos errores de tipos salen aquí,antes de ejecutar nada.El mismo .dll funciona en Windows,Linux y macOS con el runtime.

C# se compila en dos pasos: el compilador revisa el código y genera un archivo de código intermedio, y el runtime lo traduce a código máquina mientras se ejecuta. Esquema: decodigo.com.

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:

TipoQué guardaEjemplo
intenteros de 32 bits, hasta unos 2,100 millonesint edad = 31;
longenteros de 64 bitslong poblacion = 8_100_000_000;
doubledecimales en coma flotante, rápidos pero aproximadosdouble pi = 3.14159;
decimaldecimales exactos, para dinerodecimal precio = 19.99m;
booltrue o falsebool activo = true;
charun carácter, entre comillas simpleschar inicial = 'A';
stringtexto, entre comillas doblesstring 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.

Ojo con la coma. Este curso se ejecutó en un contenedor sin idioma configurado, y por eso los decimales salen con punto. En un PC con Windows en español, 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.

A la izquierda, dos variables int: copiar a en b duplica el valor y cambiar b no afecta a a. A la derecha, dos variables de una clase: copiar a en b copia la referencia, las dos apuntan al mismo objeto y un cambio por b se ve por aTipos de valor y de referencia al copiarint a = 5; int b = a; b = 9;a5b9Cada variable guarda su propio valor:cambiar b no toca a.var a = new Punto(); var b = a;arefbrefun objetoX = 9Las dos variables apuntan al mismoobjeto: b.X = 9 también cambia a.X.

Al copiar un tipo de valor se copia el dato; al copiar uno de referencia se copia la flecha, y las dos variables comparten el mismo objeto. Esquema: decodigo.com.
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 aviso
salida
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

ParaEscribeRecuerda
Ejecutar un archivodotnet run hola.csdesde .NET 10; sin proyecto
Crear un proyectodotnet new console -o MiAppel .csproj lleva la configuración
Una variablevar n = 5; o int n = 5;el tipo no cambia nunca
Dinerodecimal precio = 19.99m;double es aproximado
Texto a númeroint.TryParse(texto, out int n)Parse lanza si falla
Algo que puede faltarstring?, int?el aviso CS8602 no es un adorno
Un valor por omisiónx ?? "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.