Está en la página 1de 223

Aprende C# desde Cero

Aprende los fundamentos de C# y .NET desde las bases


hasta la Programación Orientada a Objetos

Gabriel Moroni

The Coder Cave


Copyright © 2022 Gabriel Moroni

Todos los derechos reservados

Queda prohibida la reproducción total o parcial de este libro, por cualquier medio o procedimiento sin
la expresa autorización del autor.

Transcripción y edición: Ignacio Bohmer


Sobre el autor

Gabriel Moroni (Argentina, 1988) es el fundador de la comunidad The Coder Cave y se dedica tiempo
completo al mundo del software desde hace más de 15 años. Es Software Architect y Scrum Master,
aunque se ha desempeñado en todos los roles del proceso de desarrollo.
A lo largo de su carrera ha construido una gran variedad de soluciones utilizando diversas
tecnologías y lenguajes de programación, pero se ha especializado en tecnologías .NET en los últimos
diez años.

Siempre ha tenido una gran pasión por enseñar y compartir conocimiento. Cree firmemente que en
la programación esta el futuro por lo que decidió crear The Coder Cave: un espacio para formar a
programadores de todos los niveles.

The Coder Cave tiene el objetivo de mejorar de manera tangible la vida de un millon de
programadores.

Si quieres ser parte de la comunidad, puedes seguirme en cualquiera de mis redes (Youtube,
Twitter y TikTok principalmente), entrar al Discord de la comunidad o ingresar a
www.thecodercave.com en donde encontrarás cursos completos, tutoriales y mucho material que te
ayudará a llevar tu carrera al siguiente nivel.

¡Te esperamos!
Contents

Title Page
Copyright
Sobre el autor
Aprende C# Desde Cero
Introducción
Variables
Constantes
Tipos de Datos Primitivos
Conversión De Tipo de Datos
Operadores
Comentarios
Clases
Tipos de Datos no Primitivos
Tipos de Variable
Estructuras de Control
Colecciones
Programación orientada a objetos
Conceptos Avanzados
Aprende C# Desde Cero
Introducción
¿Que es .NET?

Definir qué es .NET suele resultar confuso para los recién iniciados, así
que intentaré dar la definición más simple posible: .NET es una plataforma de
desarrollo. Ni más ni menos.
En otras palabras, .NET es un conjunto de lenguajes, librerías, herramientas y
otras plataformas, es decir, un ecosistema completo de desarrollo. Es gratuito
y de código abierto.
Con .NET podemos crear todo tipo de aplicaciones, desde aplicaciones
web hasta aplicaciones móviles pasando por Machine Learning, desarrollo de
videojuegos, aplicaciones de escritorio y aplicaciones en la nube.
Además .NET ofrece un marco de desarrollo común, lo cual permite al
desarrollador utilizar las mismas habilidades y conocimientos para escribir
una aplicación para una Raspberry Pi, una aplicación para un dispositivo
móvil o un sitio web para ser desplegado en la nube.
Esta última característica es una de las mayores ventajas de aprender
.NET: una vez que conoces los fundamentos de alguno de sus lenguajes y sus
librerías, no hay límites en cuanto a lo que puedes construir.

.NET es multiplataforma es decir que no importa en qué lenguaje escribes


tu código, según la implementación de .NET que elijas, tu aplicación podrá
ejecutarse de manera nativa en Windows, MacOS, Linux, Android o iOS.
Lenguajes Soportados

.NET soporta tres lenguajes de programación: Visual Basic .NET, C# y


F#.
Los desarrolladores podemos escribir aplicaciones .NET en cualquiera de
estos lenguajes sin ningún cambio sustancial en el proceso, más allá de las
particularidades de cada uno de los lenguajes.

Visual Basic .NET es el más antiguo de los tres, tiene una larga tradición
y evoluciona del antiguo Visual Basic que seguro has escuchado alguna vez.
Microsoft lanzó Visual Basic .NET en 2002 justamente para convertirlo en
un lenguaje amigable, fuertemente tipado y orientado a objetos.

C# es, sin dudas, el más popular de los lenguajes del ecosistema .NET.
Es un lenguaje simple, moderno, completamente orientado a objetos
(aunque también soporta las características principales de la programación
funcional) y además es fuertemente tipado.
Es uno de los lenguajes más utilizados del mundo, especialmente en entornos
corporativos.

F# es un lenguaje multiplataforma de programación funcional, también es


fuertemente tipado y además incluye elementos de la programación orientada
a objetos y de la programación imperativa.

Los tres son lenguajes de código abierto.


Breve Historia y Futuro de .NET

.NET Framework es el nombre de la primera versión de .NET liberada


en el año 2002. Esta versión de la plataforma servía para construir y ejecutar
aplicaciones exclusivamente sobre Windows.
A lo largo de casi 12 años el proyecto fue creciendo y sucesivas versiones
del Framework fueron apareciendo hasta llegar a la última versión disponible
del Framework que es la 4.8. Si bien aún es posible trabajar en aplicaciones
que estén en el Framework .NET para mantener la retro compatibilidad con
aplicaciones antiguas que todavía no han migrado a las nuevas versiones de
la plataforma, Microsoft ya ha anunciado que no habrá nuevas versiones del
Framework .NET.

Pese a las grandes ventajas que .NET Framework tenía, uno de sus
mayores problemas era que no funcionaba en ningún otro sistema operativo
fuera de Windows. Esta era además una de las principales razones por las
cuales la adopción de .NET no tenía la escala que Microsoft buscaba. Es por
esto que en el año 2014 Microsoft anuncia .NET Core, una re-
implementación de .NET Framework pero multiplataforma.

La primera versión de .NET Core estuvo disponible recién en 2016 y


permitió a los desarrolladores .NET escribir aplicaciones para otros sistemas
operativos además de Windows, tales como Linux o MacOS. A su vez, las
sucesivas versiones de .NET Core fueron agregando más y más librerías
nativas hasta conformar un enorme ecosistema de desarrollo que, a la fecha,
permite escribir prácticamente cualquier aplicación casi sin usar librerías
externas.
.NET Core tuvo varias versiones, siendo .NET Core 3.1 la última LTS
(Long Term Support) que recibirá actualizaciones por parte de Microsoft sólo
hasta Diciembre del 2022.

Cuando llegó el momento de liberar una nueva versión de .NET Core,


Microsoft pensó que utilizar el número 4 en el nombre de la versión (por
ejemplo .NET Core 4.0) sería confuso ya que la última versión de .NET
Framework es 4.8, por lo cual decidió unificar completamente la
nomenclatura y llamar simplemente “.NET” a las próximas versiones de la
plataforma.

La primera versión en utilizar esta nueva nomenclatura fue .NET 5,


liberada en Noviembre del año 2020. A partir de esto, todas las nuevas
versiones de la plataforma serán nombradas en números consecutivos (.NET
6, .NET 7, .NET 8, etc).

En la práctica, desde el punto de vista del desarrollador, no hay casi


ninguna diferencia entre programar en una versión de la plataforma o en la
otra, lo que cambiará entre versiones son las herramientas con las que el
desarrollador cuenta (mientras más antigua la versión, menos herramientas) y
la compatibilidad que la aplicación escrita tendrá con otros sistemas
operativos distintos de Windows.
Arquitectura de una Aplicación .NET

No dedicaremos mucho tiempo de este libro en explorar a detalle cómo


funciona internamente la plataforma .NET pero sí vamos a explorar
brevemente cómo es la arquitectura de una aplicación .NET y cuales son sus
elementos principales.
Lo importante es entender la transformación que sufre el código desde
que lo escribimos hasta ser ejecutado por el sistema operativo.
Como ya vimos, una aplicación .NET puede ser escrita en C#, F# y Visual
Basic .NET. Luego, el código será procesado por el compilador de lenguaje y
compilado al C.I.L. (Common Intermediate Language) que, como su nombre
lo indica, es un lenguaje intermedio, agnóstico del lenguaje elegido. El
código compilado es almacenado en assemblies, que básicamente son
archivos de extensión .dll o .exe.
Finalmente cuando la aplicación se ejecuta, el C.L.R. (Common
Language Runtime) toma los assemblies y utiliza el compilador Just in Time
para convertir estos assemblies en código de máquina que pueda ser
ejecutado por el sistema operativo particular sobre el cual la aplicación se
está ejecutando.
Entorno de Desarrollo

Para escribir aplicaciones .NET tenemos varias alternativas para elegir


según nuestros gustos o preferencias.
Visual Studio es el entorno de desarrollo de aplicaciones .NET por
excelencia, ya que fue construido por Microsoft exclusivamente para
desarrollar en su plataforma. Es una de las opciones más poderosas en cuanto
a cantidad de herramientas y características especiales a las que podemos
acceder.
A la hora de descargar Visual Studio tenemos varias versiones para
elegir, siendo Community su alternativa gratuita.
Visual Studio es compatible con cualquier versión de Windows, pero para
quienes prefieran desarrollar en Mac, Visual Studio ofrece su versión original
completamente portada y compatible con el sistema operativo de Apple.

Para los amantes de la flexibilidad aparece Visual Studio Code, también


desarrollado por Microsoft enteramente en TypeScript pero ya con la
intención de convertirlo en un entorno de desarrollo más versátil con el que
se pueda trabajar en cualquier lenguaje y no sólo en los lenguajes de .NET.
En Visual Studio Code podemos desarrollar todo tipo de aplicaciones .NET
con muy poca configuración inicial.

Además de estos tres enormes entornos de desarrollo, quienes prefieran


usar una línea de comandos para programar sus aplicaciones, .NET también
ofrece un CLI (Command Line Interface) para que no exista la necesidad de
usar una interfaz gráfica.

Y finalmente para los que no quieran moverse de sus editores preferidos


como Atom, Sublime Text o Vim, el proyecto Omni Sharp contempla un
conjunto de librerías para que tengas una gran experiencia a la hora de
programar aplicaciones .NET con estas herramientas.

Cualquiera sea el editor que elijas, .NET está orientado al desarrollador y


realmente busca hacer su vida más fácil. Comenzar a trabajar en la
plataforma es sencillo e intuitivo ya que sea cual sea el tipo de el tipo de
aplicación que elijas, en general, la configuración inicial estará lista y podrás
enfocarte en escribir tu código sin tener que preocuparte por las
particularidades del sistemas sobre el cual se ejecute.

En definitiva, para empezar a programar en .NET sólo tienes que elegir


un lenguaje, un entorno de desarrollo y el tipo de aplicación que quieres
escribir. De todo lo demás se encargará la plataforma.
Variables
Introducción

Las variables son el elemento fundamental de todo lenguaje de


programación. A partir de las variables y de su combinación con los
operadores y las estructuras de control (que veremos pronto) se construye la
lógica de cualquier sistema.
Una variable se puede entender esencialmente como un contenedor de
información. Dentro de ese contenedor podemos almacenar información,
obtener la información almacenada y también modificarla.
Una definición más técnica sería que una variable es en realidad un
espacio de memoria con un nombre específico que almacena el valor de un
tipo de datos particular. Este espacio de memoria puede luego ser accedido
siempre que se utilice su nombre.

En C# una variable se declara así:

[data type] [identifier];

Primero el tipo de datos, después el nombre de la variable y por último el


punto y coma.
Por ejemplo, esta sería la declaración de la variable customer, que es de
tipo string:

string customer;

Esta es la forma más básica de declarar una variable: separar ese espacio
de memoria para agregarle el valor más adelante, siempre y cuando ese valor
sea del tipo de datos declarado. El tipo de datos string sólo permite almacenar
valores de tipo texto en la variable.

Otra forma de declarar una variable es dándole valor en la misma


declaración:

[data type] [identifier] = [value];


string customer = “John Wick”;

El operador “igual” (=) se utiliza para asignar valor a la variable. A la


derecha del operador igual estará el valor que le será asignado a la variable,
en este caso “John Wick”.

C# también tiene algunas reglas a la hora de nombrar variables.


Primero que nada, el nombre de cada variable debe ser único para un
mismo contexto, esto significa que podemos tener el mismo nombre para una
variable pero sólo en distintas clases o en distintos métodos. Para simplificar,
por ahora digamos que el nombre de las variables debe ser único.
Además, solo pueden contener letras, números y guiones bajos, deben
comenzar siempre con una letra y son case sensitive, por lo que si tuviéramos
dos variables llamadas name y Name, ambas variables serían totalmente
diferentes.
Los nombres de las variables tampoco pueden contener palabras
reservadas y por convención se utiliza la notación camel case, por lo que en
el caso de las variables name y Name, la manera correcta de nombrar la
variable según la convención, sería name.
Si el nombre de la variable está compuesto por más de una palabra, la
primera palabra comienza con minúsculas y el resto comenzará en
mayúsculas, sin guiones para separar palabras. Por ejemplo:
customerFirstName, aquí tenemos tres palabras para describir una variable,
la primera con minúsculas y el resto con mayúsculas como separador de
palabras.
Práctico

Ahora pasemos al entorno de desarrollo y escribamos algunas variables.

Este es el clásico “Hola Mundo” en C#, que imprimirá esa cadena de


texto en la consola.

using System;

static void Main(string[] args)


{
Console.WriteLine("Hello World!");
}

Este es el resultado:

Hello World!

Pero veamos qué pasa si creamos una variable, le asignamos un valor e


imprimimos eso por consola.
Empecemos por una variable de tipo string que vamos a llamar firstName
y le vamos a asignar directamente un valor.

using System;

static void Main(string[] args)


{
string FirstName = "John";
Console.WriteLine(FirstName);
}

En este caso, el resultado que veremos por pantalla será el nombre


“John”.

John

Repasemos lo que hicimos: creamos una variable de tipo string o, en otras


palabras, asignamos un espacio de memoria llamado firstName y en ese
espacio de memoria colocamos un valor de tipo string, es decir, una cadena
de texto (John). Lo siguiente que hicimos fue utilizar directamente el valor de
esa variable simplemente colocando su nombre.
Entonces lo que vemos impreso en la pantalla es el valor de la variable.
Según el tipo de datos que una variable tiene, podremos realizar distintas
acciones con la variable, por ejemplo dos variables de texto podrían
concatenarse:

using System;

static void Main(string[] args)


{
string FirstName = "John";
string LastName = "Doe";
Console.WriteLine(FirstName + LastName);
}

Si ejecutamos la aplicación, obtendremos “JohnDoe”:

JohnDoe

Concatenar no es más que colocar el valor de un variable junto al de otra.


Y como nuestras dos variables son de tipo texto, también podríamos
concatenar directamente texto, sin usar otra variable.

using System;

static void Main(string[] args)


{
string FirstName = "John";
string LastName = "Doe";
Console.WriteLine("Name:" + FirstName + LastName);
}

Esto nos daría como resultado una nueva cadena de texto: “Name:
JohnDoe”
Name: JohnDoe

Nada nos impide agregar un espacio entre las dos variables para hacer el
texto más legible.

using System;

static void Main(string[] args)


{
string FirstName = "John";
string LastName = "Doe";
Console.WriteLine("Name:" + FirstName + " " + LastName);
}

Ahora sí podemos ver como la concatenación de variables y cadenas de


texto nos entrega un resultado mucho más interesante.

Name: John Doe

Lo que vimos hasta aquí hace referencia a variables de tipo texto, en las
que el operador “+” tiene el efecto de concatenar cadenas de texto, pero si
utilizamos el mismo operador para variables de tipo numérico, lo que
obtendremos será una clásica suma.

using System;

static void Main(string[] args)


{
string FirstName = "John";
string LastName = "Doe";
Console.WriteLine("Name:" + FirstName + " " + LastName);

int number1 = 5;
int number2 = 6;
Console.WriteLine(number1 + number2);
}

Al ejecutar estas líneas, lo que veremos en consola será 11, el resultado


de la suma de 5 y 6.

11

Es decir que tenemos dos variables de tipo int, inicializadas con valores 5
y 6, y lo que imprimimos en consola es la suma de esos dos valores.

Un detalle importante a tener en cuenta es que C# es un lenguaje


fuertemente tipado.
Esto quiere decir que sólo podemos asignar a la variable un valor acorde a su
tipo de datos.

Por ejemplo, si escribiéramos algo así:

int number = “John Ford” ;

El validador de sintaxis arrojaría un error y la aplicación no compilaría.

En estos casos, el validador de sintaxis nos advierte que no puede


convertir implícitamente un tipo string a un tipo int, ya que el espacio de
memoria que declaramos para la variable number sólo puede recibir tipos de
datos numéricos compatibles con el tamaño de un entero, y no una cadena de
texto.
En lenguajes fuertemente tipados, cuando declaramos la variable, además
de separar el espacio de memoria, también definimos qué tipo de contenido
tendrá ese espacio de memoria. Una vez declarada la variable no se puede
cambiar el tipo de datos del contenido del espacio de memoria.

En C#, las variables pueden ser declaradas e inicializadas en la misma


línea o también pueden ser declaradas sin valor de inicialización.
Es decir que estas tres sentencias son equivalentes:

int number1 = 10;


int number2;
Number2 = 10;

Ambas variables (number1 y number2) tienen un valor de 10.


Lo único a tener en cuenta en el segundo caso, es que las variables deben
ser inicializadas antes de poder ser utilizadas, de lo contrario, el compilador
devolverá un error de tipo “variable no asignada”. Mientras esto último se
cumpla, el valor de la variable puede cambiar todas las veces que lo
necesitemos.

Con un ejemplo quedará más claro:

int number1 = 5;
Console.Writeline(number1);
number1 = 20;
Console.Writeline(number1);

Ejecutando este código obtendremos dos impresiones en consola:

5
20

El valor de la variable number1 cambia luego de la primera impresión y


por eso, al imprimir la variable nuevamente, su nuevo valor (20) aparece en
la consola.

Lo mismo aplica para variables de tipo texto o cualquier otro tipo de


datos.

using System;

static void Main(string[] args)


{
string anyname = "Frank";
anyname = "Mark";
Console.WriteLine(anyname);
}

En el ejemplo anterior, lo único que veremos por consola una vez


ejecutemos la aplicación, será el último valor que la variable tiene asignado,
ya que obviamente cada valor de la variable sobreescribe el anterior.
Mark

También podemos declarar e inicializar variables del mismo tipo de datos


al mismo tiempo en única línea:

int a, b = 10, c = 50;

En este caso, la variable a todavía no tiene ningún valor asignado, la


variable b tiene un valor de 10 y la variable c tiene un valor de 50.
Esto también podría escribirse en múltiples líneas y funcionaria de igual
manera.

int a,
b = 10,
c = 50;

Este es un buen ejemplo de cómo en C# el único separador de sentencias


es el punto y coma y no el salto de línea: el compilador considera que todo es
una sola línea hasta que encuentra el punto y coma final.

Otra cosa que podemos hacer en C# es asignar a una variable el valor de


otra variable del mismo tipo.

using System;

static void Main(string[] args)


{
int a, b = 10, c = 50;

Console.WriteLine(b);
b = c;
Console.WriteLine(b);
}

A continuación, después de ejecutar el código, deberíamos ver como


primer valor 10 y como segundo valor 50 (el compilador imprime el valor
original que es 10 y luego al cambiar el valor original e imprimirlo una vez
más obtendremos por consola ambos valores: el original y el más reciente).
10
50

Esto solo puede hacerse con variables del mismo tipo.


Constantes
Introducción

En las variables, como su nombre lo indica, su valor es variable y puede


cambiar, en cambio las constantes son lo opuesto: cuando se declara una
constante su valor debe ser asignado inmediatamente y después de eso ya no
se puede cambiar. Se dice que son inmutables.
Para declarar una constante, debemos anteponer la palabra clave const.

Ejemplo:

const int thiswontchange = 42;

En general las constantes se utilizan cuando necesitamos tener un


elemento cuyo valor no cambie nunca dentro del flujo de la aplicación, ni
siquiera por accidente.
Dado que las constantes deben ser declaradas e inicializadas inmediatamente
y no pueden cambiar luego, el valor de la constante debe ser una expresión
que el compilador pueda evaluar en tiempo de compilación, esto implica que
valores numéricos booleanos y de texto son los únicos que pueden ser
utilizados como constantes.
Práctico

Dentro del framework .NET hay un gran abanico de constantes


predefinidas. La más conocida y la que generalmente se pone de ejemplo al
aprender constantes es el número PI, que forma parte de la librería Math.
Si quisiéramos imprimir el valor de la constante PI podríamos escribir:

using System;

static void Main(string[] args)


{
Console.WriteLine(Math.PI);
}

Y veremos algo como esto en la consola:

3.141592653589793
Lo que obtenemos como resultado es el valor de la constante.
El valor de PI siempre será el mismo y el hecho de que sea una constante
nos garantiza que durante su uso nunca se verá alterada de ningún modo por
la aplicación y podemos usarla cuando queramos así sin más.

Si inspeccionamos la definición de esta constante en la librería Math,


veremos algo como esto:

public const double PI = 3.1415926535897931;

Aquí aparece un elemento nuevo que no habíamos visto hasta ahora: la


palabra clave const.
Siempre que necesitemos crear una constante, es decir, un elemento cuyo
valor no puede puede ser alterado ni cambiado (intencional o
accidentalmente) por la aplicación, tendremos que usar la palabra clave const.

Así podríamos crear nuestra propia constante:

using System;
const string ConfigName = "This config will never change";
Console.WriteLine(ConfigName);

Al ejecutarlo obtendremos el valor de nuestra constante.

This config will never change

Al igual que las variables, las constantes pueden adoptar cualquiera de los
tipos de datos soportados por C#.

using System;

const int MonthsInAYear = 12;


Console.WriteLine(MonthsInAYear);

En este ejemplo usamos la cantidad de meses en un año como el valor de


nuestra constante. Al ejecutarlo obtendremos 12 como resultado.

12
Tipos de Datos Primitivos
Introducción

El tipo de datos es el encargado de avisar al compilador de C# qué tipo de


datos puede almacenar una variable. Además, como ya mencionamos antes,
C# es un lenguaje fuertemente tipado, y esto significa que el valor de una
variable de tipo concreto no se puede usar como si fuera de otro tipo. Este
concepto quedará claro más adelante cuando lo veamos en el apartado
práctico.

Esta es la lista de los datos primitivos con los que cuenta C#:

Keyword / Alias .NET Type


byte System.Byte
short System.int16
Int System.int32
long System.int64
float System.Single
double System.Double
decimal System.Decimal
bool System.Boolean
char System.Char

En realidad C# tiene algunos tipos de datos primitivos más pero no son


relevantes para este libro ya que es muy probable es que nunca los necesites.
Esta lista contiene todos los tipos de datos que necesitas saber para trabajar
con C#.

Ahora los veamos un poco más de cerca.


Tipos Numéricos Integrados

Por un lado tenemos los tipos numéricos integrados, también conocidos


como números enteros. Todos ellos admiten operadores aritméticos, lógicos y
de comparación de igualdad, que veremos más adelante a detalle.
Keyword .NET Type Range Size
byte System.Byte 0 to 255 8 bits
short System.Int16 -32.768 to 32.767 16 bits
int System.Int32 -2B to 2B 32 bits
long System.Int64 64 bits

Las dos primeras columnas son intercambiables y da lo mismo cual


usemos, es decir que estas expresiones son equivalentes:

int a = 100;
System.Int32 a = 100;

Como se ve en la tabla, la principal diferencia entre los distintos tipos de


datos de este grupo es básicamente el tamaño que se le asigna al espacio de
memoria.
Si la variable solo usará valores de entre 0 y 255, entonces con byte será
más que suficiente, en cambio short nos permitirá almacenar valores que van
desde -32.768 hasta 32.767, es decir que podrá también contener números
negativos, aunque ocupa el doble de espacio que una variable tipo byte.
El tipo de datos más comúnmente usado es int y es el que seguramente
verás siempre que una variable entera sea utilizada en C#. Los posibles
valores de este tipo de datos están en un rango muy amplio que está entre -2B
y 2B. Se podría decir que int es el tipo de datos estándar para enteros.
Si el valor que necesitas almacenar es mayor a dos billones entonces
necesitas un long, que tiene un rango de valores tan amplio que es hasta
bastante difícil de pensar. De más está decir que el espacio que ocupa en
memoria es mucho más grande también.
El valor predeterminado para todo este grupo de tipos de datos es cero y
esto es importante porque, en el futuro cuando necesites saber si una variable
de alguno de estos tipos de datos tiene o no valor, bastará con preguntar si el
valor es igual a cero o no.
Cuando decimos valor predeterminado nos referimos al valor que tiene la
variable cuando no ha sido inicializado explícitamente pero no te preocupes
que esto lo veremos más adelante en la parte práctica.
Tipos Numéricos de Punto Flotante

Keyword .NET Type Precision Size


float System.Single 6 to 9 digits 4 bytes
double System.Double 15 to 17 digits 8 bytes
decimal System.Decimal 28 to 29 digits 16 bytes

Los tipos numéricos de punto flotante son también denominados números


reales y, al igual que con los números enteros, los tipos de datos de números
reales también admiten operadores aritméticos, lógicos y de comparación de
igualdad.
Este grupo de tipos de datos funciona de manera similar a la de números
enteros, sólo que en este caso pueden incluir valores decimales. Por supuesto
que, mientras más detalle sea necesario, más espacio de memoria ocupará el
tipo de datos y más precisión permitirá tener.
Por ejemplo, el tipo de datos float permite almacenar hasta 9 dígitos y
ocupa sólo 4 bytes en memoria mientras que double permite almacenar hasta
17 dígitos por exactamente el doble de espacio. En cambio decimal admite
hasta 28 dígitos por 16 bytes.
En general decimal es el más usado para cálculos monetarios y financieros
justamente por ser el más preciso y para el resto de los escenarios es más
común encontrar valores de tipo double. El valor predeterminado de los
números reales también es cero.
Booleano

Keyword .NET Type


bool System.Boolean

La palabra clave bool representa un valor booleano que puede ser true o
false (verdadero o falso) y se utiliza así:

bool ok = true;
bool ok = false;

Las variables de tipo bool son iguales a cualquier otra variable con la
diferencia de que, en este caso, sólo pueden almacenar estos dos valores: true
o false.
Enseguida lo veremos en el práctico pero por ahora tienes que saber que
el resultado de los operadores de comparación e igualdad siempre serán de
tipo bool.
El valor predeterminado del tipo bool si no se inicializa explícitamente es
false.
Char

Keyword .NET Type Size


char System.Char 16 bytes

Char representa un carácter unicode utf 16, por ejemplo, una letra:

char a = “m”;

Para verlo de una manera más simple y gráfica, puedes pensar en el tipo
string como en una secuencia de caracteres char. Es decir que char se puede
entender como una cadena de texto de un único carácter. El tipo char admite
operadores de comparación, igualdad, incremento y decremento.
Conversión De Tipo de Datos
C# es un lenguaje fuertemente tipado, lo que significa que después de
declarar una variable y definir su tipo de datos esa variable no se puede
volver a declarar ni se le puede asignar un valor de otro tipo de datos, a
menos que este sea convertible de forma implícita.
Por ejemplo: un valor de tipo string no se puede convertir implícitamente
a uno de tipo int de la misma forma en la que una variable declarada como int
no se le puede asignar una cadena de texto. Intentarlo daría un error de
compilación como este:

Error: Cannot implicitly convert type “string” to “int”

Aún conociendo esta limitación, muchas veces sucede que igualmente


necesitamos copiar el valor de una variable en una variable de otro tipo,
especialmente cuando nosotros sabemos que la conversión es posible.

string number = “123”;

Para realizar este tipo de conversiones en C# tenemos cuatro alternativas:

Por un lado podemos utilizar conversiones implícitas. Una conversión


implícita no requiere de una sintaxis especial básicamente porque no hay
riesgo de pérdida de datos. En general las conversiones implícitas se realizan
entre tipos enteros; lo veremos más adelante en la práctica.

También podemos usar conversiones explícitas, las cuales requieren un


paso extra conocido como casteo, en donde indicamos explícitamente a qué
tipo de datos necesitamos que el valor de la variable sea convertido. Este tipo
de conversión es necesario siempre que haya un riesgo de pérdida de
información en la conversión, o cuando puede que la conversión falle por
otros motivos.
Un ejemplo típico de conversión explícita sería una conversión numérica
desde un tipo de datos con mucha precisión (ej: decimal) a uno con mucha
menos precisión (ej: float).
Además, están las conversiones definidas por el usuario que no son otra
cosa más que métodos o funciones escritas por el usuario para realizar
conversiones entre tipo de datos que en principio son incompatibles.
El proceso sería: crear un método que reciba cierto tipo de datos por
parámetro, luego escribir lógica para transformar ese tipo de datos en uno
nuevo estableciendo nuestras propias reglas y por último devolver el nuevo
tipo de datos en nuestra función.
No veremos este tipo de conversión en profundidad en este libro porque nos
interesa más bien conocer las opciones que la plataforma .NET propone y no
tanto las soluciones personalizadas, pero es importante mencionar esta
posibilidad.

Por último tenemos las conversiones con clases de asistente que se


utilizan para realizar conversiones entre tipos de datos no compatibles como
pueden ser enteros y otros objetos. Para esto se utilizan clases como
System.Convert, System.Bitconverter o los métodos parse de los tipos
numéricos integrados.

La forma más gráfica de entender las conversiones implícitas es con


tipos numéricos integrados: se dice que se puede realizar una conversión
implícita siempre que el valor que se va a almacenar pueda entrar en la nueva
variable sin que tenga que truncarse ni redondearse. Por ejemplo, nosotros
sabemos que el tipo de datos int tiene un tamaño de 32 bits y que long tiene
un tamaño de 64 bits. A partir de esto es lógico pensar que si tenemos una
variable de tipo int entonces esa variable puede almacenarse sin problemas en
otra de tipo long, y esto es posible principalmente por que nosotros sabemos
(y obviamente el compilador también sabe) que toda variable de tipo int cabe
en cualquier variable de tipo long sin riesgo de pérdida de datos porque la
variable de tipo long es más grande y tiene un mayor espacio en memoria
para almacenar datos.

int number = 123456;


long bignumber = number ;

Esta es una conversión implícita porque no es necesario agregar nada más


que la asignación y así sin más el compilador será capaz de realizar la
conversión. Con esto se puede deducir que toda variable de tipo numérico se
puede convertir implícitamente a otra cuyo tamaño sea mayor y la contenga
de modo tal que no se pierda la información en el proceso.
Pero no siempre podemos hacer conversiones en donde no hay riesgo de
perder información. Cuando no se puede realizar una conversión sin riesgo de
perder información, entonces el compilador necesita que se realice una
conversión explícita. Volvamos al ejemplo anterior y demos vuelta los
valores. Imaginemos que ahora la variable inicial es de tipo long y que
queremos almacenar su valor en otra variable de tipo int. Si intentamos hacer
una conversión implícita el compilador arrojará un error, y si lo piensas por
un segundo tiene sentido que así sea porque lo que el compilador hace es
advertirnos que en esta conversión puede haber una pérdida de información
ya que estamos tratando de almacenar el valor de una variable de gran
tamaño en otra de menor tamaño.

long number = 123456;


int second number = number; ERROR

Pero en este ejemplo nosotros bien sabemos que no habrá pérdida de


información porque el valor de la variable number es lo suficientemente
pequeño como para ser asignado a secondNumber, por lo que podríamos
indicar al compilador que estamos seguros de que queremos proceder con la
conversión a nuestro riesgo. La manera de hacerlo es a través de un cast, que
es simplemente el acto de especificar entre paréntesis el tipo al cual se va a
convertir el dato justo delante del valor o la variable que se va a convertir.
Se dice conversión explícita por que tenemos que especificar
explícitamente cuál es el tipo de datos al cual queremos convertir el valor de
la variable.

int second number = (int) number;

Hasta ahora hemos convertido números enteros que son del mismo grupo
de tipos de datos, pero qué pasa si volvemos al ejemplo del comienzo de la
lección en donde tenemos un string que nosotros sabemos que puede ser
convertido a un tipo int. Para convertir este valor de tipo texto a un valor
numérico podemos llamar al método parse o al método tryparse del tipo
numérico al cual queremos convertir la variable. Cada uno de los tipos
numéricos que hemos estudiado hasta aquí posee estos dos métodos parse y
tryparse, los cuales pueden usarse indistintamente con la única diferencia de
que el método tryparse devolverá un valor booleano indicando si la
conversión fue exitosa o no, además del resultado de la conversión.

string txtNumber = “1234”;


int number = int32.parse(textnumber);

En cambio el método parse lanzará una excepción si la conversión no se


puede realizar, lo que cortará el flujo de la aplicación.
Otra alternativa sería utilizar los métodos de la clase convert que contiene
métodos para convertir los valores de tipo string a cualquier valor numérico
que se necesite.
Práctico

Para comenzar, vamos a declarar una variable de tipo byte, la más


pequeña de todas las variables numéricas. Como sabemos que byte acepta
valores entre cero y 255, le daremos simplemente un valor de cien. Luego
declararemos una variable de tipo int y le asignaremos el valor de la variable
byte con una conversión implícita (es decir, sin hacer mucho más que la
asignación).

using System;

byte num = 100;


int secondnumber = num;

Console.WriteLine(secondnumber);

Luego imprimimos el valor de la segunda variable y esto es lo que vemos


por consola:

100

El resultado es el esperado por lo que la conversión implícita ha


funcionado.
Ahora hagamos algo diferente: cambiemos el tipo de datos de las variables
exactamente al revés. Lo que antes era una variable de tipo byte ahora es un
int y viceversa.

using System;

int num = 100;


byte secondnumber = num;

Console.WriteLine(secondnumber);

Al hacer este cambio, el comportamiento de nuestro código ha cambiado


fundamentalmente porque num es de tipo int que, como sabemos, tiene 32
bits, y estamos intentando convertirlo en un tipo byte que tiene tan sólo 8. La
respuesta que obtendremos a esto es que la aplicación nos advertirá de una
potencial pérdida de información y arrojará un error como este:

Cannot implicitly convert type 'int' to 'byte'. An explicit conversion exists


(are you missing a cast?)

En español: “no se puede convertir implícitamente el tipo int en un tipo


byte. Ya existe una conversión explícita (has olvidado una conversión?).”
En otras palabras, no podemos realizar una conversión implícita de tipo
int a tipo byte, la única manera de lograrlo es con una conversión explícita.
También se nos dice entre paréntesis que quizás olvidamos un casteo, lo cual
pone en evidencia lo que decíamos antes: el compilador nos advierte de que
la conversión que estamos tratando de hacer tiene el potencial de fallar o de
acarrear una pérdida de información, es por eso que debemos agregar un
casteo, lo cual automáticamente convierte esta asignación en una conversión
explícita.

using System;

int num = 100;


byte secondnumber = (byte)num;

Console.WriteLine(secondnumber);

Al agregar (byte) el error desaparece y – en este ejemplo particular– la


conversión funciona porque el valor asignado a la variable num es un número
que está dentro del rango aceptable para un byte, por lo que no hay pérdida
de datos.

Ahora bien, no pasaría lo mismo si el valor de num pasará de ser 100 a


ser 1000:

using System;

int num = 1000;


byte secondnumber = (byte)num;
Console.WriteLine(secondnumber);

Al ejecutar este código comprobaremos que se cumple la advertencia del


compilador y perdemos información en el proceso:

232

Esto sucede por que el espacio de memoria asignado a la variable


secondnumber de tipo byte no tiene el tamaño suficiente para almacenar el
valor requerido por la variable num,es por eso que hay que tener cuidado en
este tipo de conversiones y asegurarse de que entendemos el riesgo y que
estamos seguros de que la potencial pérdida de información no es relevante
para lo que estamos haciendo.

Ahora pasemos al último tipo de conversión y tomemos el ejemplo que


veíamos en la parte teórica. Vamos a suponer que tenemos un string que tiene
un valor que podría convertirse perfectamente a un int o a un número. La
alternativa más sencilla es utilizar el método Parse de la clase Int32 si
queremos convertirlo en tipo int.

using System;

string txtnumber = "1234";

int num = Int32.Parse(txtnumber);


Console.WriteLine(num);

Esta conversión funciona perfectamente:

1234

Además, como ya sabemos, la variable int es un alias de la clase Int32,


por lo cual es posible escribir esto mismo pero utilizando simplemente int en
lugar de Int32.

using System;

string txtnumber = "1234";


int num = Int32.Parse(txtnumber);
Console.WriteLine(num);

El resultado será el mismo porque la operación es exactamente la misma.

En general se recomienda que este tipo de expresiones sea utilizada en


conjunto con un pequeño código para manejar excepciones, especialmente
cuando no estemos totalmente seguros de que la conversión irá bien. Esto es
útil porque cuando la conversión no es posible por algún motivo, el método
Parse disparara una excepción.

En este punto es importante introducir una estructura muy conocida: el try


catch.

using System;

try
{

}
catch (Exception)
{

Esta estructura tiene dos bloques: un bloque try y un bloque catch.


Dentro de las llaves del try se coloca el código que queremos que el
motor de .NET intente ejecutar y dentro de las llaves el catch se coloca el
código que queremos que se ejecute en caso de que una excepción ocurra
dentro de las llaves del try.
Con un ejemplo los conceptos siempre quedan más claros:

using System;

try
{
string txtnumber = "1234";
int num = Int32.Parse(txtnumber);

Console.WriteLine(num);
}
catch (Exception)
{
Console.WriteLine("something went wrong");
}

Veamos a detalle esta última porción de código.


Dentro de las llaves del try escribimos aquello que pensamos que puede
producir una excepción. Si todo funciona como esperamos no habrá
problemas, el código se ejecutará normalmente y podremos ver en la consola
el valor de num, pero si en algún momento de ese bloque de código que está
entre las llaves del try se produce un error, entonces la secuencia se
suspenderá y el código que se ejecutará a continuación será el que hayamos
escrito dentro del catch.
En este caso veríamos el código personalizado de error que acabamos de
escribir, con el texto “Something went wrong…”.
En este ejemplo particular no habrá ningún error (lo sabemos porque lo
probamos algunos párrafos antes) por lo que al ejecutar este código veremos
que la conversión se ha realizado con éxito y el código dentro del catch no se
habrá ejecutado. Es decir que no veremos nuestro mensaje personalizado.

A continuación probemos forzar un error para que quede claro en qué


escenarios esto puede fallar. Para esto voy a modificar el valor de la variable
de tipo string y le voy a asignar texto en lugar de un número.

using System;

try
{
string txtnumber = "Mark";
int num = Int32.Parse(txtnumber);

Console.WriteLine(num);
}
catch (Exception)
{
Console.WriteLine("Something went wrong");
}
Al ejecutar esta pequeña aplicación de consola, la conversión falla y el
código del catch se ejecuta. Ahora sí vemos por consola el mensaje de que
algo salió mal.

Something went wrong

Esto significa que el código que estaba dentro del try (que se supone era
el flujo principal de la aplicación) ha lanzado una excepción. Ni más ni
menos.
Es aquí donde aparece la ventaja de usar el método tryparse para realizar
este tipo de conversiones. Veámoslo con un ejemplo.

Vamos a dejar de lado el try/catch por un momento así que lo


eliminaremos de nuestra aplicación de consola y en cambio escribiremos el
tryparse que, como verás, tiene una sintaxis muy particular.

using System;

string txtnumber = "Mark";


int number;
bool ok = int.TryParse(txtnumber, out number);

Console.WriteLine(number);

Dos puntos importantes para explicar aquí.


Primero que nada, el método devuelve un valor bool que me permite
saber si la conversión se pudo realizar o no, es por esto que agregamos una
nueva variable ok que contendrá verdadero o falso según el resultado de la
conversión. Esto quiere decir que la aplicación no lanzará ninguna excepción
si la conversión falla sino que simplemente devolverá “falso”.
Lo segundo es que ahora la variable que quiero que contenga la
conversión (number) debe ser pasada al método como argumento, antecedida
por la palabra clave out, que indica que el parámetro será pasado por
referencia (esto lo veremos a detalle más adelante).

Ahora con menos líneas de código nos aseguramos de que la conversión


suceda y a la vez nos aseguramos de que si no sucede, la aplicación no
fallará.
Si ejecutamos este código, sólo obtenemos un cero, que es el valor por
defecto de los números enteros.
Tal como sospechábamos, la conversión no fue posible.

Ahora pasemos a la última de las opciones que es la clase convert. Esta


clase posee un método para cada tipo de datos y recibe siempre un string.
Entonces si volvemos al valor numérico de la variable txtnumber,
podemos convertirlo así:

using System;

string txtnumber = "1234";


int number = Convert.ToInt32(txtnumber);

Console.WriteLine(number);

Al ejecutarlo comprobaremos que la conversión ha funcionado.

1234

Pero no sólo es posible convertir cadenas de texto a enteros, esto mismo


aplica para cualquier otro tipo de datos. Dos ejemplos:
De float a single:

using System;

string txtnumber = "1234";


float number = Convert.ToSingle(txtnumber);

Console.WriteLine(number);

De string a bool (siempre que sea posible):

using System;

string txtnumber = "true";


bool ok = Convert.ToBoolean(txtnumber);
Console.WriteLine(ok);

Al ejecutar esto último, el valor de la variable incluso se corrige porque


aparece en mayúsculas y eso es justamente por que el valor real del booleano
está en mayúsculas.

True
Operadores
Los operadores son símbolos especiales que ejecutan ciertas acciones
sobre los operandos tal y como funcionaría en matemáticas. Por ejemplo, el
símbolo más (+) realiza la suma entre el número que está a la izquierda del
operador y el que está a la derecha.
C# ofrece varios operadores para distintos tipos de operaciones, algunos
de ellos incluso modifican levemente su funcionamiento según el tipo de dato
de los operandos:

int a = 10 + 10;
int b = a + 5;
string c = “Hola” + “Mundo”;

En este ejemplo, el mismo símbolo más (+) que primero suma los valores
de tipo entero, luego concatena las dos cadenas de texto en la línea que sigue,
es decir que con el mismo símbolo obtenemos resultados distintos según el
tipo de datos de los operandos.
Operadores Aritméticos

Los operadores aritméticos pueden realizar operaciones aritméticas con


todos los operandos numéricos tales como byte, short, int, long, float, double
y decimal.

Operator Name Example


+ Addition int x = 5 + 5;
- Subtraction int x = 5 - 4;
* Multiplication int x = 5 * 2;
/ Division int x = 100 / 5;
% Reminder int x = 5 % 2;
++ Unary Increment X++;
-- Unary Decrement X--;

La tabla ejemplifica claramente lo que hace cada operador en la columna


de ejemplo.

No es ninguna novedad decir que el símbolo más (+) representa una


adición (5 + 5) de igual forma que el símbolo menos (-) representa una
sustracción (5 - 4).
Para multiplicación y división es lo de siempre: multiplicación se
representa con un asterisco (*) y división con una barra (/). El operador
denominado reminder representa el resto de la división entre los operandos y
se aplica con el símbolo porcentual (%), mientras que las dos últimas filas de
la tabla son incremento y decremento por unidad respectivamente. Es decir
que para el incremento (X++), el operador va a agregar una unidad más a X,
entonces si X es 5, luego de aplicar el incremento por unidad el valor de X
será igual a 6. Exactamente lo mismo pasa con el decremento (X--) que va a
restar una unidad al valor de la variable X de forma tal que si X es 5, luego de
aplicar el decremento por unidad el nuevo valor de X será 4.
Operadores de Comparación

Los operadores de comparación comparan dos valores numéricos y


devuelven un valor booleano verdadero o falso según corresponda.

Operator Example
< x < y;
> x > y;
<= x <= y;
>= x >= y;

Analicemos un poco más de cerca los ejemplos de la tabla.


El primer operador devolverá verdadero sólo si el operando de la derecha
es menor que el operando de la izquierda. En otras palabras: si x es menor a y
entonces devolverá verdadero, pero si x es mayor a y devolverá falso.
Exactamente lo opuesto sucede en la segunda fila: si x es mayor a y, el
resultado será verdadero pero si x es menor a y, será falso.
Y los dos últimos ejemplos son iguales con la diferencia de que en este
caso se agrega el operador igual (=) que agrega una nueva comparación: si x
es menor o igual que y devolverá verdadero, de lo contrario devolverá falso.
Para el último ejemplo, la comparación será verdadera sólo si x es mayor o
igual que y.
Como ves, aquí lo que importa es el orden en el cual ponemos los
operandos a comparar.
Operadores de Igualdad

Los operadores de igualdad se encargan de determinar si dos operandos


son iguales o no. Son los operadores más comunes de todos los que hemos
visto hasta ahora y son los que más veces te encontrarás en tu carrera como
desarrollador.

Operator Example
== x == y;
!= x != y;

Leer los ejemplos de este operador es bien sencillo: la operación de la


primera fila sólo devolverá verdadero si x es igual igual a y, mientras que la
operación de la segunda fila sólo devolverá un valor booleano verdadero si x
es distinto a y.
Operadores Lógicos

C#, como tantos otros lenguajes de programación, también incluye


operadores lógicos booleanos.

Operator Logical Operator Example


! NOT !false
&& AND x && y;
|| OR x | | y;

El primero es el símbolo de exclamación de cierre, que puede parecer un


poco complicado aquí en la teoría pero tendrá mucho más sentido en la
práctica.
Lo que este operador hace es dar vuelta el resultado de una expresión
booleana: por ejemplo, si x es una variable booleana y tiene un valor falso, al
aplicar este operador el resultado será verdadero.
En definitiva, siempre que este operador se aplique a una variable o
expresión, cualquiera sea el resultado booleano de esa expresión, una vez el
operador se aplique el resultado será exactamente opuesto: si era verdadero
será falso y si era falso será verdadero.
A continuación tenemos las dos claves de sol (&&) que representan la
operación lógica AND. Esta operación resultará verdadera si y sólo si ambos
operandos booleanos son verdaderos. Sino, resultará falsa.
Este operador es muy importante para comparar expresiones y
asegurarnos de que ambas condiciones se cumplen. A la hora de ejecutar el
código, si el compilador encuentra que la expresión de la izquierda es falsa,
ni siquiera evaluará la de la derecha y directamente devolverá un resultado
falso, sin importar si la expresión de la derecha es verdadera o no; en el
operador lógico AND ambas expresiones deben ser verdaderas.
Y por último tenemos el operador lógico OR que son las dos líneas rectas
(||) denominadas pipes en inglés.
En este operador bastará con que al menos una de las condiciones sea
verdadera para devolver un resultado verdadero, a diferencia del AND en
donde ambas expresiones tienen que ser verdaderas para que el resultado sea
verdadero.
Práctico

Operadores Aritméticos

using System;

int a = 5;
int b = 5;

Console.WriteLine(a + b);

Este código no necesita mayores explicaciones. Cuando se trata de


valores numéricos, el operador “+” sumará los valores tal cual lo
esperaríamos. En este ejemplo lo que veremos por consola será la suma de
los valores de las variables a y b.

10

Lo mismo sucede con el resto de los operadores aritméticos cuando


trabajamos con variables de tipo numérico.
Por ejemplo, si en lugar de sumar quisiéramos multiplicar los valores de
las variables a y b, deberíamos escribir algo así:

int a = 5;
int b = 5;

Console.WriteLine(a * b);

Con el operador asterisco (*) conseguimos lo que buscábamos.

25

En C# también podemos combinar operaciones en una o más líneas.


Que tal si por ejemplo creamos una nueva variable c y a esta variable le
asignamos el resultado de la suma de a y b. Luego podemos dividir esta
nueva variable c por el valor de la variable b. Suena más complicado de lo
que es.

using System;

int a = 5;
int b = 5;
int c = a + b;

Console.WriteLine(c / b);

El resultado que obtenemos es 2:

Entonces hemos comprobado que podemos guardar el resultado de una


operación aritmética en otra variable del mismo tipo (en este caso entero) y
que, a su vez, a esta nueva variable podemos aplicarle otra operación
utilizando el valor de una de las primeras variables. A partir de esto aparecen
infinitas combinaciones porque podemos usar y reusar las variables cuantas
veces sea necesario.

Es importante notar que los operadores aritméticos funcionan igual que


una operación aritmética. Por ejemplo si nosotros hiciéramos a + b * c en una
operación matemática, por más que la suma aparezca antes que la
multiplicación entre c y b, la multiplicación tendrá prioridad y por lo tanto a
será sumada al resultado entre la multiplicación de b y c. Como b * c = 50, el
resultado final será 55.

using System;

int a = 5;
int b = 5;
int c = a + b;

Console.WriteLine(a + b * c);

En consola vemos el resultado esperado:


55

Tal cual funciona en una operación matemática común, para cambiar el


orden en el cual se ejecutan los operadores tenemos que usar paréntesis.

using System;

int a = 5;
int b = 5;
int c = a + b;

Console.WriteLine((a + b) * c);

Con los paréntesis hemos alterado el orden por lo que primero se sumarán
a y b, y luego el resultado será multiplicado por c.
El resultado final también cambia: ahora es 100.

100

Como conclusión podemos decir que en las operaciones aritméticas de C#


las reglas de la aritmética normal también se aplican.
Operadores de Comparación

Al programar en cualquier lenguaje de programación, frecuentemente te


encontrarás con la necesidad de comparar el valor de dos o más variables. La
respuesta que obtengas siempre tomará la forma de un valor booleano
verdadero o falso.
Una buena práctica al implementar comparaciones es hacerlo a modo de
pregunta.
Por ejemplo, siguiendo con las variables que teníamos en la lección
anterior, podríamos preguntar: ¿es b mayor a c?

using System;

int a = 5;
int b = 5;
int c = a + b;

Console.WriteLine(b > c);

La respuesta es “falso” porque b es 5 y c es 10.

False

Ahora cambiemos la pregunta y veamos si b es igual a c. Recordemos que


la forma de comparar si un valor es igual a otro es utilizando dos signos
iguales (==) porque un signo igual (=) se usa para la asignación, por lo tanto
para comparación tenemos que agregar otro.
Entonces: ¿es b igual a c?

using System;

int a = 5;
int b = 5;
int c = a + b;

Console.WriteLine(b == c);
Evidentemente no.

False

Ahora demos vuelta la comparación inicial y preguntemos ¿es b es menor


a c?

using System;

int a = 5;
int b = 5;
int c = a + b;

Console.WriteLine(b < c);

Finalmente la comparación devuelve verdadero.

True

Hasta aquí estudiamos lo básico de la comparación pero ahora


agreguemos un poco más de complejidad y combinemos los operadores.
Preguntemos ¿es b menor a c y también a mayor a c?

using System;

int a = 5;
int b = 5;
int c = a + b;

Console.WriteLine(b < c && a > c);

Las dos claves de sol (&) representan el operador lógico AND, y para que
éste operador lógico valide como verdadero, ambas expresiones, tanto la de la
izquierda como la de la derecha, deben ser verdaderas. El compilador
evaluará las dos expresiones y devolverá verdadero o falso según
corresponda.

False
Para obtener un resultado verdadero, tenemos que cambiar el comparador
porque sabemos que a es menor a c y que también b es menor a c.

using System;

int a = 5;
int b = 5;
int c = a + b;

Console.WriteLine(b < c && a < c);

Ahora sí la expresión evaluará verdadero.

True

Recuerda que los operadores lógicos sólo pueden devolver valores


booleanos que pueden ser tan sólo verdadero o falso.

Ahora volvamos un segundo a la expresión anterior que era falsa.

using System;

int a = 5;
int b = 5;
int c = a + b;

Console.WriteLine(b < c && a > c);

¿Qué pasa si cambiamos el AND por un OR?


El operador lógico OR se representa con dos barras verticales (||) también
conocidos cómo pipes.

using System;

int a = 5;
int b = 5;
int c = a + b;

Console.WriteLine(b < c || a > c);


Al ejecutarlo el resultado final cambia y lo que antes devolvía falso
utilizando el AND, ahora es verdadero ¿por qué?

True

Porque al operador OR sólo le basta con que una de las expresiones sea
verdadera para que el resultado sea verdadero. De hecho algo que es muy
importante saber es que si tenemos dos expresiones (como en este caso) y la
primera ya evalúa verdadero, el compilador no ejecutara la expresión que
sigue por que no lo necesita.
En otras palabras, lo que está a la derecha no será evaluado nunca si lo
que está a la izquierda es verdadero.

También es posible combinar todo esto con el operador NOT.


Por ejemplo agreguemos un paréntesis que cubra toda la expresión y ya
dentro de ese paréntesis agreguemos un NOT.

using System;

int a = 5;
int b = 5;
int c = a + b;

Console.WriteLine(!(b < c || a > c));

Como sabemos que el resultado de esta operación es verdadero, ahora al


aplicarse el NOT obtendremos un falso porque C# invertirá el resultado de la
expresión.

False

El operador NOT también es un operador de comparación por lo tanto


también devuelve sólo un verdadero o un falso.

Volvamos por un momento a una versión más simple de esta expresión


para realizar una nueva pregunta ¿es c distinto de b?
Aquí también utilizamos el operador de negación sólo que esta vez lo
utilizamos directamente en la comparación.
using System;

int a = 5;
int b = 5;
int c = a + b;

Console.WriteLine(c != b);

Y cómo c efectivamente es distinto de b, el resultado que obtendremos


será verdadero.

True

Cómo para darle una última vuelta de tuerca ya que sabemos que la
respuesta es verdadera, envolvemos la expresión entre paréntesis y la
negamos.

using System;

int a = 5;
int b = 5;
int c = a + b;

Console.WriteLine(!(c != b));

Básicamente estamos invirtiendo el resultado booleano, así que lo que


antes era verdadero ahora después de habérsele aplicado el operador lógico
NOT, es falso.

Los operadores te acompañarán a lo largo del camino así que es


importante entenderlos y practicarlos. Lo más recomendable es tomarse el
tiempo de jugar con las diferentes variables y combinaciones de expresiones
y valores y recordar que un lenguaje de programación se aprende por
repetición y práctica más que por cualquier otra cosa.
Comentarios
A simple vista este puede parecer un capítulo menor cuando hablamos de
aprender un lenguaje de programación, pero ser capaz de comentar
apropiadamente tu código es una de las mejores prácticas a la hora de
programar. Ten en cuenta que en la programación profesional, ya sea que
trabajes para una empresa o un proyecto de código abierto, muy rara vez
serás la única persona trabajando en una determinada porción de código, lo
más probable es que el código que escribas sea leído e interpretado por
muchos otros programadores después de tí. Además, tú mismo tendrás que
leer código que han escrito otros y agradecerás cuando esté bien comentado,
es por eso que acompañar la lógica que escribas con un comentario relevante
es una práctica que es bueno adoptar desde el principio.
En C# existen varias alternativas para agregar comentarios según lo que
necesitemos comentar. La más común y la que más te encontrarás en
aplicaciones en las que trabajes, es el comentario de una sola línea.
Este comentario se consigue colocando dos barras inclinadas (//) al principio
de la línea y rápidamente entenderás que el código está comentado porque
toda la línea tomará otro color. Si estás trabajando en Visual Studio, el color
del comentario será verde.

//int x = 10;

Eso significa que esta línea no será evaluada por el compilador en tiempo
de compilación por lo que puedes escribir lo que quieras ahí. Todo lo que
sigue a las dos barras no será evaluado por el compilador pero recuerda que
esto sólo aplica a esa única línea; en cuanto presiones enter y saltes de línea,
el código volverá a ser evaluado normalmente.
Al utilizar el comentario de una única línea, no necesariamente tienes que
comentar la línea completa, también puedes agregar un comentario a la mitad
de la línea, a continuación de tu código.

int x = 10; //This is a comment about x

Como decíamos: a partir de las dos barras inclinadas, el resto de la línea


no será evaluada por el compilador.
Si lo que necesitas es comentar más de una línea, entonces tiene sentido
usar el comentario multilínea. Para agregar este tipo de comentario, en lugar
de escribir las dos barras inclinadas como prefijo en cada línea, directamente
escribimos una secuencia de caracteres para abrir y otra secuencia de
caracteres para cerrar; todo lo que esté en el medio será tratado como
comentario sin importar cuantas líneas ocupe.

/*
int a = 10;
int b = 10;
*/

El último tipo de comentario que podemos usar en C# es el comentario


XML, que no es diferente a los que ya hemos visto, sólo que contiene XML
embebido y generalmente se usa para documentar código a un alto nivel para
luego ser exportado en alguna otra herramienta.
Para crear este tipo de comentarios, sólo tenemos que pararnos encima
del nombre de una clase o de un método y escribir tres barras inclinadas
seguidas (///).

using System;

namespace ConsoleApp1
{
/// <summary>
///
/// </summary>
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World");
}
}
}

En este caso podemos ver que arriba de la clase aparecen varias líneas de
comentario y un tag XML que se llama Summary. Es decir que apareció un
comentario multilínea con un tag XML dentro del cual podemos agregar los
comentarios particulares que necesitemos.

Si hacemos esto mismo pero encima de un método con parámetros,


veamos lo que pasa:

using System;

namespace ConsoleApp1
{
/// <summary>
///
/// </summary>
class Program
{
/// <summary>
///
/// </summary>
/// <param name="args"></param>
static void Main(string[] args)
{
Console.WriteLine("Hello World");
}
}
}

Además del tag summary, ahora ha aparecido un tag param para el


argumento args que nos da la posibilidad de explicar a detalle de qué se trata
este parámetro.
En resúmen, estos son los comentarios XML que nos ayudan a explicar
de forma acabada de qué se trata la clase, el método y cada uno de sus
parámetros.

Como decíamos al principio, los comentarios son un aspecto fundamental


de la programación tanto para tí como para los demás programadores que
trabajen en tu código. No tienen ningún costo extra en términos de
performance y tampoco tienen ningún impacto para el usuario final ya que
son ignorados por el compilador, así que siempre que puedas te recomiendo
comentar tu código cuando sea necesario.
Clases
Introducción

C# es un lenguaje completamente orientado a objetos y tanto es así que en


C# todo es un objeto. Por eso tiene sentido que aprendamos qué es una clase
y qué es un objeto como parte de los conceptos fundamentales de C#.

Cuando hablamos de clases y objetos podemos pensar, por ejemplo, en


una clase verdura y en cada una de las verduras como objetos de la clase
verdura. O también podríamos pensar en una clase libro en donde cada libro
específico que se nos ocurra podría ser declarado como una instancia de esa
clase.
Una clase describe todos los atributos de los objetos y también los
métodos que definen el comportamiento de esos objetos. Es decir que
podríamos entender a las clases como plantillas o templates de objetos, y a
los objetos como instancias de una clase, es decir, como representaciones
únicas e individuales de esa clase.

Pensemos en el segundo ejemplo.


Cuando hablamos de una clase libro, todos sabemos a qué nos referimos.
Todos los libros tienen tapa, tienen portada, tienen un cierto número de
páginas, capítulos, autores, etc.
Todas estas propiedades estarán definidas en la clase, pero los valores
particulares de cada una de estas propiedades estarán definidas en cada
objeto. Harry Potter no tendrá la misma cantidad de páginas que Moby Dick,
ni tampoco tendrá la misma imagen de portada. El valor concreto de la
variable número de páginas definida en la clase, estará en cada uno de los
objetos, es decir, la variable número de páginas (o el espacio de memoria
asignado para el número de páginas) estará en la clase, pero el valor
particular del número de páginas estará en el objeto.
Entonces podríamos decir que una clase es un grupo de métodos y
variables relacionadas entre sí. Una clase describe algo determinado y en la
mayoría de los casos uno puede crear instancias de esa clase, en donde cada
una de estas instancias es lo que conocemos como objeto, y dentro de estos
objetos se pueden utilizar todas las variables y los métodos antes definidos en
la clase.
A partir de una clase se pueden crear tantas instancias como sean
necesarias y no hay restricciones para esto.
Pronto vamos a explorar en detalle todos los elementos de una clase, uno
por uno, pero ahora vamos a darle un vistazo general a los elementos de una
clase:

class Person
{
private string name;

public Person (string name)


{
this.name = name;
}
public string name
{
get { return name; }

set { name = value; }


}
public string ToString()
{
return "This person is called" + name;
}

}
}

Esta es la clase Person.


Contiene una sola variable de tipo string llamada name, que ha sido
declarada como privada. Esa palabra clave private que está justo antes de
string (el tipo de datos) significa que esa variable name no puede ser accedida
por fuera de la clase.
Esto es lo que se denomina scope de una variable.
Pero si miramos más abajo, aparece la propiedad name, que está
declarada de acceso público gracias a la palabra clave public que está justo
antes del tipo de datos.
Esta propiedad permitirá que la variable name sea accedida desde afuera
de la clase, pero no a través de la variable directamente, sino a través de la
propiedad, que es la única con permisos de modificar la variable.
Además, esta clase Person tiene un constructor, que no es más que un
método con el mismo nombre de la clase (se puede ver justo debajo de la
variable name). El constructor sirve para indicar qué valores queremos que se
pasen cómo argumentos cuando se cree una instancia de una clase.
En este ejemplo el argumento del constructor también es un string, que
luego se utiliza para actualizar el valor de la variable name. Como hay un
solo constructor en esta clase, entonces los objetos derivados de la clase
Person sólo pueden instanciarse si se les especifica un nombre,o en otras
palabras, si se especifica un valor para la variable name.
El último de los métodos, llamado ToString, simplemente devolverá una
cadena de texto con la información que hay en la clase sobre esta persona.
Como vimos hasta aquí toda la información que esta clase mantiene es el
nombre de la persona, pero además de devolver sólo ese nombre podemos
devolver una cadena de texto más larga y con un mensaje un poco más
amigable.
Esta es la definición de la clase de Person. Todo lo que pueda parecer
confuso hasta aquí, tendrá sentido muy pronto cuando analicemos cada
elemento a detalle.

Y así es como se crea una instancia de la clase Person:


Person person = new Person("John");

En esta instancia de la clase Person, el objeto person tendrá de nombre


“John”, que es el valor de name que se agrega a través del constructor.

Luego podríamos recuperar este nombre invocando al método ToString,


con lo cual el valor de la propiedad se imprimirá por consola.

Console.WriteLine(person.ToString());
Fields/Campos

El elemento más simple que puede tener una clase es field o simplemente
campo en español.
Volvamos al ejemplo de la clase Person.

class Person
{
private string name;
}

En esta clase teníamos una variable llamada name de tipo string: esto es
lo que se conoce como field.
Para definirlo técnicamente diremos que en realidad un field es toda
variable que esté declarada dentro de una clase. Tan simple como eso.
Es decir que la diferencia principal entre una simple variable y un field es
el lugar en donde la variable está declarada y aunque a simple vista parezca
trivial, esta es una diferencia importante porque está relacionada con lo que
conocemos como el scope de una variable, que recién mencionamos casi al
pasar.
El scope de la variable establece desde donde puede ser accedida esa
variable, por lo que una variable local definida dentro de un método
cualquiera sólo podrá ser accedida y utilizada dentro de ese método
específico.
En cambio un field puede ser accedido desde cualquiera de los métodos
de la clase y hasta desde métodos de otras clases si la visibilidad lo permite,
cosa que en este caso no sucede porque tiene un modificador de acceso
privado (private). Si cambiamos el modificador de acceso de privado a
público (public) la variable sí que podría ser accedida desde cualquier lugar.
Sabiendo esto es importante aclarar que la forma recomendada de acceder
a los campos de una clase desde afuera de la clase es a través de propiedades
que veremos a continuación.
Por eso verán que, en general, los campos se declaran en las clases arriba
de todo y son de tipo privado.

En el ejemplo, la variable name no tiene valor inicial así que, para poder
usarla, habrá que asignarle un valor.
Además de usar propiedades, otra opción muy frecuente es la de utilizar
el constructor público de la clase para asignarle un valor al campo:

class Person
{
private string name;

public Person (string name)


{
this.name = name;
}
}

En el ejemplo aparece una palabra que no habíamos visto hasta aquí, la


palabra clave this. this se usa para indicar que la variable a la cual queremos
acceder está dentro de la misma clase. Si miramos detenidamente el ejemplo,
notaremos que tanto el campo como el argumento que el constructor recibe
por parámetros tienen el mismo nombre, por lo tanto usamos this para indicar
al compilador que la variable que será asignada es la que está declarada a
nivel de clase, es decir, el campo.
El constructor se lee como sigue: dado un string name recibido por
parámetros, el valor de la variable name se le asigna al campo name de la
clase Person.

Este trabalenguas puede parecer un poco confuso pero si uno lo piensa


detenidamente tiene bastante sentido: se declara una variable name de tipo
string que sólo podrá ser usada dentro del método constructor y no fuera ya
que su scope es tan solo el constructor. Luego el field name de la clase
Person cuyo scope es toda la clase, recibirá el valor de la variable name local
del constructor. De esta forma se asigna valor a un campo que es privado para
la clase, desde afuera de la clase, porque el constructor es público.
Todo esto cobrará más sentido conforme avancemos con las lecciones, la
intención aquí es introducir de a poco los conceptos.
Propiedades

Hasta aquí aprendimos que un field es una suerte de variable global en


una clase que puede ser accedida desde cualquier método. También dijimos
que había algunas formas de acceder a ese campo privado desde afuera de la
clase, como por ejemplo cambiando su modificador de acceso de privado a
público (dijimos que esto no era recomendable) o utilizando un constructor.
Sin embargo la manera recomendada de acceder a un campo por fuera de
clase es a través de una propiedad.

class Person
{
private string name;
public string name
{
get {return name;}
set {name = value;}
}
}

Una propiedad es un punto intermedio entre un campo y un método.


Al igual que un campo, tiene un modificador de acceso (generalmente public
o protected), un tipo de datos y un nombre, pero a diferencia de un campo, la
propiedad tiene un cuerpo que permite controlar de manera más precisa el
comportamiento del acceso al campo.

Miremos en detalle el ejemplo de arriba. Dentro de la propiedad tenemos


dos palabras claves: get y set que pueden ser entendidas como los “métodos”
de la propiedad.
Cuando queremos leer el valor de un campo lo hacemos con get y cuando
queremos asignar el valor al campo con el set. Claro que también se pueden
crear propiedades con tan solo el método get o con tan solo el método set
para conseguir propiedades de sólo lectura o de sólo escritura.
El ejemplo conforma el caso más clásico de cómo propiedad y campo
funcionan juntos: el método get devuelve el valor del campo y el método set
asigna el valor al campo name.
Dentro del cuerpo del set se utiliza la palabra clave value que hace
referencia al valor que la propiedad recibe para actualizar el campo.
La ventaja de las propiedades es que el campo permanece privado y sólo
puede ser actualizado o retornado a través de su propiedad, por lo cual el
control sobre el valor del campo es total ya que el único lugar en el que se
actualiza está centralizado en la propiedad.

Más allá del ejemplo clásico que vimos, los métodos de una propiedad
pueden ser tan complejos como se nos ocurra. Consideremos un caso en
donde el valor del campo no sea asignado y devuelto así sin más, sino que,
por el contrario, se han modificado justo antes de ser devuelto y justo antes
de ser asignado.

private string name;


public string name
{
get {return “The name is “ + name;}
set
{
if (value.contains(“,”);
throw new Exception (“Error”);
name = value;
}
}

Ahora la propiedad se ve completamente diferente.


Primero que nada, más allá del valor que tenga el campo, el método get
siempre entregará una cadena de texto de la forma “The name is”, sumado al
valor del campo. Es decir que hay una modificación en la forma en la que se
devuelve el valor y cuando la propiedad sea accedida por fuera de la clase,
esa será la forma que tomará.
Lo mismo sucede con el método set. En esta implementación particular, si
el valor de value contiene una coma, ocurrirá un error y el valor no será
asignado. Esto demuestra que podemos escribir dentro de nuestras
propiedades tanta lógica como haga falta.

Sin embargo, en la gran mayoría de los casos, las propiedades no


necesitarán un tratamiento especial ni necesitarán lógica extra más que la de
servir como un acceso público a los campos así que, en la gran mayoría de
los casos, tan solo necesitarás las propiedades que vimos en el primer
ejemplo:

private string name;


public string name
{
get {return name;}
set {name = value;}
}

Para ahorrar código y también para salvarnos del fastidio de escribir lo


mismo varias veces, .NET nos ofrece las propiedades auto implementadas en
donde este mismo comportamiento se consigue con única línea y ni siquiera
necesitamos agregar el campo explícitamente:

public string name {get; set;}

Internamente el comportamiento es exactamente el mismo que vimos


recién: existirá una variable privada que será accesible sólo a través de la
propiedad pública a través de sus métodos get y set.
Lo bueno es que en tiempo de desarrollo con tan solo escribir esta línea
de código lograremos todo eso. Esto es lo que encontrarás en la gran mayoría
de las clases.

Además, existe la posibilidad de inicializar una propiedad auto


implementada con un valor por defecto simplemente agregando el valor por
defecto a continuación de la propiedad:

public string name {get; set;} = “John Wick”;

Las propiedades auto implementadas son tan sólo un atajo; una forma de
escribir menos código y obtener el mismo comportamiento.
Visibilidad

La visibilidad de una clase, un método, una variable o una propiedad


establece cómo podrá ser accedido ese elemento.
Para determinar la visibilidad de los elementos utilizamos los
modificadores de acceso, algunos de los cuales ya hemos visto parcialmente.
Los modificadores de acceso más comunes son: private (privado) y
public (publico) pero tambien existen muchos mas:

public Accesible desde cualquier lugar


Accesible sólo dentro de la misma clase o clases que
protected
hereden de esta clase
internal Accesible dentro del mismo proyecto
protected Accesible desde otros proyectos si las clases heredan de
internal esta clase
private Accesible sólo desde esa clase

El modificador de acceso public establece la menos restrictiva de las


visibilidades: todo elemento delimitado como público puede ser accedido
desde cualquier lugar.
A continuación aparece protected que significa que los miembros sólo
pueden ser accedidos dentro de la misma clase o desde clases que hereden de
esa clase.
internal hace referencia a que los miembros sólo pueden ser accedidos
dentro del mismo proyecto, protected internal es igual a internal, sólo que las
clases que hereden de la clase marcada como protected internal pueden
acceder aún si están en otros proyectos, y por supuesto private, que establece
que el miembro puede ser accedido sólo por miembros de la misma clase. Es
el más restrictivo de todos los modificadores de acceso. Los miembros
privados son estrictamente exclusivos a esa clase sin importar que otras
clases hereden de ella, tampoco podrán acceder a miembros privados.
Métodos

Un método es una porción de código que realiza una o varias acciones y


puede o no entregar un resultado.
En otros lenguajes de programación se conocen como funciones pero en
C#, cuando pertenecen a una clase, son llamados simplemente métodos.
Los métodos son útiles porque nos permiten encapsular porciones de
funcionalidad para luego poder ejecutarlas una y otra vez desde diferentes
lugares.
Un método se define así:

<visibility> <return type> <name> (<parameters>)


{
<code>
}

Como podemos ver, en la firma del método (que es la primera línea del
método) tenemos que definir una visibilidad, un tipo de datos de retorno (es
decir, qué va a devolver
ese método si es que devuelve algo), a continuación el nombre del
método y opcional, entre paréntesis y como argumentos una lista de
parámetros.
Luego entre las llaves va a estar el código del método en sí.

Pongamos un ejemplo concreto para que sea un poco más gráfico:

public int multiplicar (int number1, int number 2)


{
return number1 * number2;
}

Aquí tenemos un método muy sencillo que recibe dos números enteros
por parámetros, los multiplica y devuelve el resultado. Si lo analizamos
palabra por palabra vamos a encontrar que el método tiene una visibilidad
pública, un tipo de retorno int (es decir que el método devolverá un valor
entero) y que también tiene un nombre que es multiplicar.
A continuación y entre paréntesis aparecen dos parámetros que son los
valores que el método recibirá, ambos de tipo int y separados por coma:
number1 y number2.
Por último y entre llaves está el código del método propiamente dicho
que en este caso no es más que la multiplicación de los valores recibidos por
parámetros.
La palabra clave return indica que esa es la última línea del método y que
justamente esa multiplicación será lo que el método devuelva.
Para invocar este método, simplemente usaremos su nombre y pasaremos por
parámetros los dos números que queremos multiplicar entre paréntesis:

multiplicar (2, 5);

Si el método no recibe ningún parámetro, entonces los paréntesis


quedarán vacíos, pero deberán escribirse de cualquier manera. Ahora bien,
como el método multiplicar ha definido que recibe dos parámetros, estos
parámetros son obligatorios y no se puede utilizar el método sin pasar estos
dos argumentos.

En muchos casos no necesitamos que el método devuelva ningún tipo de


resultado y sólo queremos que se ejecute cierta lógica. Para lograr esto
tenemos que escribir la palabra clave void (vacío en inglés) en el lugar en
donde iría el tipo de retorno del método. Esto le dirá al compilador que la
palabra clave return no aparecerá en ningún lugar de nuestro método porque
no habrá ningún valor para devolver.

Veamos un ejemplo:
public void DoSomething (int number1, int number2)
{
int number3 = number1 * number2;
Console.Writeline(number3);
}

En este caso no necesitamos que el método devuelva nada y sólo nos


interesa imprimir en consola la suma de los dos números recibidos por
parámetro.
Volvamos al método multiplicar un momento:

public int multiplicar (int number1, int number 2)


{
return number1 * number2;
}

Aquí tenemos un tipo de datos int como valor de retorno y la palabra


clave return como última línea del método. Lo importante a aclarar es que
siempre que se establezca un tipo de datos como tipo de retorno de un
método (básicamente siempre que el tipo de retorno no sea void), entonces
será obligatorio que todos los posibles caminos de este método devuelvan un
valor de ese tipo de datos; de no ser así el compilador lo advertirá con un
error.

El último elemento que nos falta introducir en la sección de métodos es el


de los parámetros opcionales. Usemos una vez más el ejemplo del método
multiplicar:

public int multiplicar (int n1, int n2)


{
return n1 * n2;
}

Como ya sabemos, este método recibe dos parámetros de tipo int: n1 y


n2.
Al tener el método dos parámetros definidos, es obligatorio que cuando se
llame al método se pasen estos dos parámetros. Si a la hora de invocar el
método no se especifican los dos parámetros (que además deben ser de tipo
int) entonces el compilador devolverá un error y la aplicación no compilara.
En conclusión: los parámetros que definimos en la firma del método luego
serán obligatorios cuando necesitemos usar el método.
Sin embargo, existe la posibilidad de agregar parámetros opcionales, es
decir, parámetros que definamos en la firma del método pero que después
podemos o no enviar cuando el método sea invocado.

public int multiplicar (int n1, int n2, int n3 = 0)


{
return n1 * n2 * n3;
}

multiplicar(2,5);

Un parámetro es opcional cuando se le define un valor por defecto en su


declaración.
En este ejemplo, el parámetro n3 tiene un valor por defecto de cero, por
lo cual si no se le suministra un valor a la hora de invocar el método, el valor
de n3 será cero cuando sea utilizado. En el ejemplo, el resultado del método
será cero porque n3 valdrá cero al momento de ser evaluado en la
multiplicación.

Pero también podemos suministrar un nuevo valor para n3 si así lo


quisiéramos:

public int multiplicar (int n1, int n2, int n3 = 0)


{
return n1 * n2 * n3;
}

Podríamos invocar este método de cualquiera de las dos maneras:

Multiplicar(2,5);
Multiplicar(2,5,8);

Esta es la ventaja de los parámetros opcionales: que pueden o no ser


utilizados.
No hay ninguna restricción en cuanto a la cantidad de parámetros opcionales
que puede tener un método, de hecho todos los parámetros del método
podrían ser opcionales si así lo necesitáramos.
Constructores

Los constructores son métodos especiales que definen cómo crear


instancias de una clase. Un constructor no puede devolver ningún valor y es
por eso que no tiene un tipo de dato de retorno como tienen los métodos
regulares.

Un método se define así:

public string MethodName(){}

Un constructor se define así:

public person() {}

Teníamos un ejemplo de constructor en este mismo módulo en la clase


Person, que tomaba un parámetro de tipo string llamado name.

public Person(string name) {this.name = name}

En una misma clase pueden convivir uno o más constructores y cada uno
de ellos hace referencia a distintas maneras de instanciar la misma clase.
Además, cada vez que se define una clase, esa clase contiene un
constructor sin parámetros, aún cuando éste NO se defina explícitamente.

public Person() {}

Siguiendo con este ejemplo, la clase Person con estos dos constructores
podría instanciarse así usando simplemente el constructor sin parámetros:

Person john = new Person();

O podría instanciarse de esta manera utilizando el constructor que recibe


un parámetro de tipo string.

Person john = new Person(“John”);


En resumen, los constructores definen la forma en la cual la clase será
inicializada.
Podemos optar por agregar parámetros o dejarlos vacíos.
Si no se especifica ningún constructor, la clase tendrá siempre un
constructor sin parámetros por defecto.
Namespaces

La palabra namespace se encuentra en prácticamente todos los archivos


que contengan código escrito en C#. En general, un namespace es
esencialmente una manera de agrupar tipos y clases en un espacio en sí
mismo y con un nombre en particular.

namespace MyOffice
{
class Person{}
class Employee{}
class Manager {}
}

Un namespace suele contener una o más clases y no es más que una


forma de ordenarlas según un cierto criterio.

En otros lenguajes de programación estos se conocen como paquetes,


pero en C# llevan el nombre de namespaces.
Ya veremos en la práctica cómo se importan los namespaces, pero por
ahora bastará con saber que cuando queremos usar una clase que pertenece a
un namespace específico desde otro namespace, entonces tendremos que
agregar en nuestro archivo, arriba de todo, la palabra clave using seguida por
el nombre del namespace.

Muchas veces sucederá que el namespace es demasiado largo y es


incómodo usarlo tal como es. Por ejemplo, si usamos la librería serialization
que forma parte del framework, este es el using que necesitamos:

using System.Web.Script.Serialization;

Podríamos evitar agregarlo arriba de todo y en lugar de eso integrarlo


directamente en la porción de código en el cual lo necesitemos. Para esto,
solo necesitamos anteponer el namespace a la clase que estamos intentando
utilizar.
Sería algo así:
var js = new System.Web.Script.Serialization.JavaScriptSerializer();

Tal como se ve es un poco incómodo ya que contiene demasiadas


palabras, por eso C# introduce el concepto de alias que nos permite ponerle
un nombre abreviado al namespace dentro del using pudiendo hacer algo
como esto:

using serialization = System.Web.Script.Serialization;

Entonces, cuando necesitemos usar este mismo namespace ahora solo


sería necesario utilizar su forma abreviada:

var js = new serialization.JavaScriptSerializer();

Es muy normal que en proyectos complejos, con muchos módulos y


clases, el nombre de los namespace sea demasiado largo y en estos casos es
en donde usamos los alias.
Práctico

En C# todo es una clase.


Un ejemplo de esto es el método main que hemos estado utilizando en
algunos de nuestros ejemplos. Seguramente no habías reparado en que aquí
también tenemos una clase. Todo este código se ejecuta dentro de una clase
llamada Program.

using System;

namespace ConsoleApp3
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}

Pero ahora escribamos nuestra propia clase.


Para hacerlo utilizaremos la palabra clave class seguida del nombre de la
clase y a continuación agregaremos las llaves. Todo lo que esté entre las
llaves formará parte de la clase.
El nombre no tiene ningún tipo de validación por parte de C# así que allí
podemos escribir lo que se nos ocurra, sin embargo es una buena práctica
ponerle nombres significativos a nuestras clases para que luego sea fácil
reconocer el propósito de un vistazo.

using System;

namespace ConsoleApp3
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}

class Customer
{

Si pensamos en la clase como en una plantilla, necesitamos pensar en


cuáles son los atributos que suele tener un cliente para poder determinar sus
propiedades. Empecemos con los atributos básicos de cualquier persona:
primer nombre, apellido y una dirección.

Agregamos el modificador de acceso, luego el tipo de datos y por último


el nombre de la propiedad. Luego entre las llaves colocamos get y set.

class Customer
{
public string FirstName { get; set; }
}

Recuerda que esta era la forma abreviada y que si bien nosotros no lo


escribimos explícitamente, para el compilador aquí existe un campo llamado
FirstName, además de una propiedad FirstName con su respectivos métodos
get y set.

Así queda nuestra clase Customer una vez que agregamos todas las
propiedades:

class Customer
{
public string FirstName { get; set; }
public string LastName { get; set; }

public string Address { get; set; }

public string Phone { get; set; }


}

Ya listas las propiedades podemos pasar a los constructores.


Como ya sabemos, los constructores representan la manera en la cual
estas clases pueden ser instanciadas. No tienen tipo de retorno, sólo
modificador de acceso y el nombre de la clase.
Es importante que el nombre del método siempre coincida con el de la
clase. En este caso elegiré uno sin parámetros y sin cuerpo.

class Customer
{
public string FirstName { get; set; }

public string LastName { get; set; }

public string Address { get; set; }

public string Phone { get; set; }

public Customer() { }
}

De esta forma le permito a esta clase ser instanciada sin pasarle ningún
parámetro. Es decir que todas estas propiedades pueden estar vacías a la hora
de crear una instancia de la clase.
Igualmente vamos a agregar un constructor más y esta vez sí que tendrá
parámetros:

class Customer
{
public string FirstName { get; set; }

public string LastName { get; set; }


public string Address { get; set; }

public string Phone { get; set; }

public Customer() { }

public Customer(string lastName)


{

}
}

Al agregar este constructor estoy definiendo que una de las formas de


crear instancias de esta clase es pasando el apellido por parámetros. Como ya
vimos, lastName es una variable local al método, lo que significa que no
puede ser usada fuera del método porque no existiría. La buena práctica dice
que el nombre del parámetro debe comenzar con minúsculas para no
confundirse con el nombre de la propiedad, por eso lo nombramos lastName.
Ahora lo que deberíamos hacer para que el parámetro tenga sentido es
asignar el valor que recibimos por parámetro a la propiedad LastName:

class Customer
{
public string FirstName { get; set; }

public string LastName { get; set; }

public string Address { get; set; }

public string Phone { get; set; }

public Customer() { }

public Customer(string lastName)


{
LastName = lastName;
}
}
Con esto ya se puede instanciar la clase Customer enviando el parámetro
lastName que luego será asignado a la propiedad de forma tal que ahora sí
toda la clase tendrá acceso a este valor.

Como no hay límites en cuanto a la cantidad de constructores que


podemos tener, ahora vamos a agregar otro más que incluya como parámetros
primer nombre y apellido del cliente:

class Customer
{
public string FirstName { get; set; }

public string LastName { get; set; }

public string Address { get; set; }

public string Phone { get; set; }

public Customer() { }

public Customer(string lastName)


{
LastName = lastName;
}
public Customer(string firstName , string lastName)
{
FirstName = firstName;
LastName = lastName;
}
}

Hasta aquí tenemos una clase Customer con cuatro propiedades y tres
constructores, y hay tres maneras de instanciar esta clase.

Ahora agreguemos también un método que devuelva el nombre completo


del cliente, usando las propiedades de la clase:

class Customer
{
public string FirstName { get; set; }

public string LastName { get; set; }

public string Address { get; set; }

public string Phone { get; set; }

public Customer() { }

public Customer(string lastName)


{
LastName = lastName;
}
public Customer(string firstName , string lastName)
{
FirstName = firstName;
LastName = lastName;
}

public string GetFullName()


{
return LastName + "," + FirstName;
}
}

Este método no recibe nada por parámetros pero sí que utiliza los valores
de las propiedades LastName y FirstName. Lo único que hace es
concatenarlos y agregar una coma en el medio de los dos.

Ya definida la clase podemos pasar a utilizarla.


Para eso volvemos a nuestro método main que, si bien está en otra clase,
puede utilizar sin problemas la clase Customer porque forma parte del mismo
namespace.

Y sabemos que hay varias formas de instanciar la clase Customer así que
comencemos por la primera:
using System;

namespace ConsoleApp3
{
class Program
{
static void Main(string[] args)
{
Customer customer1 = new Customer();
customer1.FirstName = "Tony";
customer1.LastName = "Soprano";
}
}
}

En la primera línea del método main se crea la instancia de la clase


Customer utilizando el constructor sin parámetros. Debido a que no estamos
enviando ningún parámetro, las propiedades del objeto estarán vacías, así que
voy asignar valor a sus propiedades después de que el objeto haya sido
inicializado.
Es decir que me ahorré de enviar los valores por parámetros al
constructor al momento de la inicialización, pero puedo acceder más tarde al
objeto invocando su propiedades para asignarles valor.

Entonces ahora que ya tenemos un cliente creado con nombre y apellido,


podemos invocar al método de la clase haciendo uso de Console.WriteLine()
para que se imprima el valor por pantalla.

using System;

namespace ConsoleApp3
{
class Program
{
static void Main(string[] args)
{
Customer customer1 = new Customer();
customer1.FirstName = "Tony";
customer1.LastName = "Soprano";

Console.WriteLine(customer1.GetFullName());
}
}
}

El resultado que veremos por pantalla será: Soprano,Tony.

Ahora probemos crear un nuevo objeto de tipo Customer.


Esta vez el nombre de instancia será customer2 porque es un objeto
diferente e independiente del anterior y en lugar de usar el constructor sin
parámetros usaremos el constructor que espera un apellido en la inicialización
del cliente:

using System;

namespace ConsoleApp3
{
class Program
{
static void Main(string[] args)
{
Customer customer1 = new Customer();

customer1.FirstName = "Tony";
customer1.LastName = "Soprano";

//Console.WriteLine(customer1.GetFullName());

Customer customer2 = new Customer("Malkovich");


Console.WriteLine(customer2.GetFullName());
}
}
}
(He dejado la definición de customer1 para que puedan comparar ambas instancias aquí mismo y
he comentado la línea en la cual se imprime el valor de customer1 por si estás copiando y pegando este
código en tu entorno de desarrollo. Digamos que aquí sólo importa el valor de customer2 que se
imprime por pantalla)

El resultado que obtendremos ahora por consola será sólo Malkovich ya


que customer2 no tiene FirstName, aunque bien podríamos agregarlo igual
que antes:

using System;

namespace ConsoleApp3
{
class Program
{
static void Main(string[] args)
{
Customer customer1 = new Customer();

customer1.FirstName = "Tony";
customer1.LastName = "Soprano";
//Console.WriteLine(customer1.GetFullName());

Customer customer2 = new Customer("Malkovich");


customer2.FirstName = "John";
Console.WriteLine(customer2.GetFullName());
}
}
}

Al ejecutar esta porción de código, ahora sí obtendremos el nombre


completo del cliente: Malkovich, John.

Todavía nos falta utilizar uno de los constructores así que vamos a crear
una nueva instancia de la clase Customer y para que las nuevas líneas de
código no se confundan con las anteriores, agregaremos un comentario
multilínea al código que teníamos hasta aquí.
Para este nuevo cliente usaremos la última de las opciones que nos
permite enviar dos cadenas de texto para nombre y apellido respectivamente.

using System;
namespace ConsoleApp3
{
class Program
{
static void Main(string[] args)
{
/*
Customer customer1 = new Customer();

customer1.FirstName = "Tony";
customer1.LastName = "Soprano";
Console.WriteLine(customer1.GetFullName());
Customer customer2 = new Customer("Malkovich");
customer2.FirstName = "John";
Console.WriteLine(customer2.GetFullName());
*/

Customer customer3 = new Customer("Mario", "Puzo");


Console.WriteLine(customer3.GetFullName());

}
}
}

Hasta aquí hemos aprendido a crear objetos de tres formas distintas (de
las infinitas que hay) y hemos obtenido siempre el mismo resultado.
Las clases son el elemento fundamental de la programación orientada a
objetos así que las veremos mucho más a lo largo de este libro.
Tipos de Datos no Primitivos
Static

Recién estudiamos que la manera habitual de utilizar una clase es creando


una instancia nueva de la clase y trabajando con el objeto para acceder a sus
métodos y propiedades.
En la gran mayoría de los casos así es como usaremos clases: creando
múltiples instancias de la misma clase según sea necesario.
Sin embargo, en algunos casos muy particulares, es útil tener una clase
cuyos elementos puedan ser utilizados sin la necesidad de que la clase sea
instanciada o, en otras palabras, sin que haga falta crear un objeto para
acceder a sus métodos y propiedades.
Por ejemplo, imaginemos una clase con una variable cuyo valor nunca
cambia, sin importar cómo y dónde se use:

public static class Calculations


{
public static int GetSum(int n1, int n2)
{
return n1 + n2;
}
}

Este es un ejemplo de una clase estática con un miembro estático.


En la definición de la clase, después del modificador de acceso public que
decide la visibilidad de la clase, se encuentra la palabra clave static, y la
misma palabra aparece de nuevo en el método GetSum antes del tipo de
retorno del método. Es decir, que tanto clase como método son estáticos.
Cuando una clase es estática, entonces puede tener miembros estáticos, es
decir, métodos y campos estáticos.
Una clase estática no puede ser instanciada por lo que, en el fondo, una
clase estática no es más que un grupo de miembros relacionados entre sí.

Un ejemplo de cómo usamos este método es el siguiente:

Console.WriteLine(Calculations.GetSum(2,5));
Como puedes ver no es necesario instanciar la clase y simplemente
escribiendo el nombre de la clase (Calculations) seguido de un punto y el
nombre del método, ya podemos usarlo. Esta es la ventaja de los métodos y
las clases estáticas: no hace falta crear un objeto para poder usar sus métodos
y propiedades.

Sabiendo esto, repasemos algunas reglas generales a la hora de usar la


palabra clave static:
● Una clase estática sólo puede contener métodos estáticos, es decir,
que el compilador devolverá a un error cuando se intente agregar un
método no estático a una clase estática.
● Una clase no estática puede contener miembros estáticos, aunque
éstos no podrán ser accedidos desde los objetos o las instancias de la
clase y la única manera de acceder a estos miembros sería el que
acabamos de ver, simplemente escribiendo el nombre de la clase
seguido de un punto y del nombre del método estático.
● Una clase no estática que contiene métodos estáticos puede ser
instancia como cualquier otra clase aunque, como decíamos, los
métodos estáticos no estarán disponibles en las instancias de la clase.
Clases Parciales

Una de las posibilidades que C# ofrece a la hora de trabajar con clases


complejas o demasiado largas, es la de utilizar clases parciales.
Cuando una clase es definida con la palabra clave partial, entonces esa
clase puede ser extendida en otra clase, que también debe ser declarada como
partial.

namespace Partials
{
partial class Car
{
public string SpeedUp()
{
return “Accelerate!”;
}
}
}

Aquí tenemos un ejemplo de la clase Car definida como partial. Hasta


aquí es una clase igual que otras que ya hemos visto y tiene un único método
llamado SpeedUp.
La ventaja que ofrece haber agregado la palabra clave partial, es que
también podemos tener otro archivo así:

namespace Partials
{
partial class Car
{
public string SlowDown()
{
return “Not so Fast!”;
}
}
}

Esta es otra clase parcial con el mismo nombre, pero con un método
distinto.
Entonces tenemos dos clases diferentes con el mismo nombre y con la
palabra clave partial o, para decirlo de otra forma, tenemos dos partes
separadas de la misma clase en distintos archivos.
Usar la clase Car será exactamente igual a lo que estábamos
acostumbrados. De hecho, a la hora de instanciar la clase, para el compilador
sólo existe una clase y no hay ninguna diferencia visible entre clases parciales
y clases no parciales en tiempo de desarrollo:

namespace Partials
{
class Program
{
static void main(strings[] args)
{
Car car = new Car();
car.SpeedUp();
car.SlowDown();
}
}
}

Desde la instancia de la clase, tendremos acceso a todos sus miembros


como si sólo existiera un único archivo.
Las clases parciales nos permiten organizar mejor el código de clases
complejas pero no tiene ningún otro impacto real en la forma en la cual las
clases funcionan. Es meramente organizacional.
Enum

Un enum o enumerador es un tipo especial de clase que representa un


grupo de constantes. Es decir, valores de sólo lectura que no pueden
modificarse.

enum Categories
{
Food,
Snacks,
Drinks,
};

Para crear un enum utilizaremos la palabra clave enum y separaremos los


valores con comas. La sintaxis para acceder a estos valores es tan sencilla
como agregar el nombre del enumerador seguido por un punto y por el valor
que queremos obtener.

Categories cat = Categories.Food;


Console.WriteLine(cat);

Y lo que aparecerá en consola será la cadena de texto de la categoría:

Food

Todos los enumeradores tienen valores enteros por defecto aunque no se


vean.
En el ejemplo de arriba, aunque no lo diga, a la categoría Food le
corresponde el valor 0. A Snacks 1 y a Drinks 2.

enum Categories
{
Food, //0
Snacks, //1
Drinks, //2
};
Para obtener este valor entero debemos hacer un cast del valor del
enumerador a un entero:

int catNumber = (int)Categories.Food;


Console.WriteLine(catNumber);

Ahora obtendremos simplemente un número:

Si necesitamos valores diferentes a los que .NET ofrece por defecto,


podemos agregarlos explícitamente en el enumerador sin que la forma de
obtenerlo cambie respecto a la anterior.

enum Categories
{
Food = 4,
Snacks = 7,
Drinks = 9
};

Ahora al obtener e imprimir el valor…

int catNumber = (int)Categories.Food;


Console.WriteLine(catNumber);

Obtenemos el número que definimos recién:

En conclusión, los enumeradores soportan un tipo de texto así como


también un tipo entero.
Funciones Locales

Las funciones locales son menos frecuentes en C# porque aparecieron


recién con C# 7 y no han estado disponibles desde el principio, es por esto
que quizás no te encuentres tantas funciones locales cuando leas código C#
que han escrito otros. A pesar de esto, creo que de todas maneras es
importante conocer esta herramienta por el gran potencial que tiene.
Aquí será relevante recordar un concepto que ya hemos visto: el scope.

public void thisIsAMethod()


{
string localvar;
}

Cuando declaramos una variable dentro de un método, esa variable sólo


será accesible dentro de ese método. No hay modificadores de acceso
posibles: cuando una variable es declarada dentro de un método, siempre será
local a ese método. En este caso, la variable localvar sólo puede ser asignada
y accedida dentro de este método. Fuera de él, no existe.
Las funciones locales representan el mismo principio, pero con métodos.
Una función local se declara dentro de un método y sólo puede ser
accedida y utilizada dentro de ese método, esto permite que la funcionalidad
sea encapsulada en el método y el código quede limpio y organizado.

public void thisIsAMethod(bool isActive)


{
string GetStatus()
{
return isActive ? “Active” : “Inactive”;
}
Console.WriteLine(GetStatus());
}

Cuando un programador lea la implementación de este método, será


evidente
qué cosas se supone que el método ejecute además de dejar perfectamente
claro que la función local sólo es aplicable en el contexto de este método y no
en otro.
Se podría decir que el principal objetivo de una función local es organizar
el código para hacerlo más legible.
Las funciones locales se declaran como cualquier otro método aunque sin
el modificador de acceso, lo cual tiene sentido porque la función no puede ser
accedida por fuera del método de todas maneras.
En este ejemplo, el método Getstatus es local al método thisIsAMethod,
por lo cual solo será accedido y solo existirá dentro de ese método.
String

Aún sin definirlo formalmente ya hemos utilizado cadenas de texto en


este libro, así que ya te darás una idea de qué hablamos cuando hablamos de
strings.
Técnicamente un string es una serie de caracteres que representa un texto.
Puede ser un solo carácter, una palabra o una frase larga delimitada por
comillas dobles

“C”

“Word”

“This is a phrase”

C# contiene un tipo de datos destinado específicamente a almacenar estas


cadenas de texto cuyo alias es string.

string c = “C”;

string w = “Word”;

string p = “This is a phrase”;

El tamaño máximo que puede ocupar un string en memoria son 2GB, que
sería el equivalente a algo así como dos billones de caracteres, aunque en la
práctica dependerá de la capacidad técnica de la computadora que contiene el
proceso. En lecciones posteriores veremos cuál es la manera más eficiente de
trabajar con cadenas de texto tan grandes.

Hasta ahora dijimos que un string está delimitado por comillas dobles
pero ¿qué pasa si necesitamos que el valor de la cadena de texto también
incluya comillas dobles?

string text = “Can i use this “string” within the string?”;

Esta línea no funcionará y el compilador entregará un error porque no


puede identificar en donde comienza el string y en donde termina. Para
resolver este tipo de problemas, C# incluye un carácter de escape de modo
que cualquier carácter que esté a continuación del carácter de escape, será
tomado como texto literal por C# y el compilador no intentará interpretarlo
sino que lo imprimirá directamente como texto.
Este carácter de escape es la barra invertida \, así que para que el string de
arriba compile habría que escribirlo así:

string text = “Can I use this\”string\”within the string?”;

¿Y qué pasaría si quisiéramos que nuestro texto contenga una barra


invertida? deberíamos escapar la barra invertida con otra barra:

string path = “\\shared\\temp\\user”;

Aunque es evidente que en un caso como éste se vuelve un poco


inconveniente agregar tantos caracteres de escape especialmente si
tuviéramos una ruta mucho más larga con muchas más barras invertidas.
Para resolver este segundo problema y de paso, para que el código no se
ensucie con tantos caracteres especiales, C# nos ofrece la posibilidad de
utilizar otro carácter especial para indicarle al compilador que no intente
interpretar nada de lo que sigue y que tome literal toda la cadena de texto.
Este carácter especial es el arroba (@) y debe ser utilizado antes de la
primera comilla doble:

string path = @“\shared\temp\user”;

Además, como ya hemos visto, los strings pueden ser concatenados


utilizando el símbolo más (+). Estos son algunos ejemplos similares a los que
ya estudiamos:

string name = “John” + “Wick”;


string firstname = “John”;
string lastname = “Wick”;
String client = “Mr” + firstname + “ ” + lastname;

En C#, el string es inmutable; esto significa que es de sólo lectura y no


puede cambiar. Una vez que ha sido creado en memoria, cada vez que se
concatena un string, el motor del .NET creará un nuevo espacio en memoria
para el string concatenado. Es por esto que para concatenaciones grandes se
recomienda usar un tipo de datos que provee el framework, llamado
StringBuilder. Estudiaremos esta clase en detalle en las próximas lecciones.

String interpolation es un operador especial para trabajar con strings que


permite hacer cosas que de otra manera sería muy complicado hacer.
Podríamos resumirlo diciendo que todo lo que String interpolation permite
es agregar en la cadena de texto determinados marcadores especiales que
luego el compilador reemplazará con los valores correspondientes.
Veamos un ejemplo porque en la práctica siempre se aclaran los
conceptos.
Supongamos que tenemos una variable cliente y además una variable
edad:

string name = “Walter White”;


string age = 40;

Y ahora supongamos que queremos usar el valor de estas dos variables en


una nueva cadena de texto. Concatenando los valores conseguimos lo que
necesitamos:

Console.WriteLine(name + “is” + age + “years old”);

Pero veamos la diferencia de hacer lo mismo usando string interpolation:

Console.WriteLine($”{name} is {age} years old”);

Como se puede ver, la segunda opción es más limpia y mucho más fácil
de leer.
Para poder aplicar esta técnica, hay que colocar justo delante de la cadena
de texto un carácter especial que es el símbolo del dinero ($). Seguido de este
carácter se coloca la cadena de texto entre comillas dobles.
De este modo las variables name y age están inyectadas dentro de la
cadena de texto, cada una de ellas encerrada entre llaves. Estas llaves le dicen
al compilador que lo que está adentro es una variable y que todo lo que está
en tres llaves debe ser reemplazado por el valor de la variable.
Pero el uso de estas llaves no se reduce al valor de una variable. A decir
verdad, las llaves pueden incluir expresiones mucho más complejas:

Console.WriteLine($” (10 + 5) * 2 = {(10 + 5) * 2}”);

En este ejemplo, el compilador evaluará la expresión que está entre llaves


y luego, en la cadena de texto, directamente veremos el resultado de la
expresión, es decir que por consola se imprimirá esta cadena de texto:

(10 + 5) * 2 = 30
Práctico

Casi desde el comienzo del libro estamos haciendo uso de strings sin
haberlos introducido propiamente, y es lógico porque un string no es más que
una variable que puede almacenar una cadena de texto. Lo interesante es
explorar algunas de las operaciones que podemos realizar con esas cadenas
de texto y algunos de los caracteres especiales de los que disponemos.

Como decíamos, una cadena de texto puede ser un único carácter o un


conjunto de caracteres: siempre que la cadena esté entre comillas dobles será
considerada como un string por el compilador.

string number = "1";


string street = "Palo Alto St";
string city = "California";

También podemos concatenar cadenas de texto para crear una nueva:

string number = "1";


string street = "Palo Alto St";
string city = "California";

string address = street + number + city;

Console.WriteLine(address);

Esto creará un nuevo string como resultado de la operación, en donde


cada uno de los valores estará pegado al otro sin ningún tipo de espacio:

Palo Alto St1California

Así que agreguemos espacio entre las cadenas de texto para que la
dirección sea más legible:

string number = "1";


string street = "Palo Alto St";
string city = "California";

string address = street + " " + number + " " + city;

Console.WriteLine(address);

Ahora esto ha concatenado cinco cadenas de texto: las tres variables más
los dos nuevos espacios.

Palo Alto St 1 California

Algo interesante es que a través de la clase String tenemos acceso a


ciertas propiedades y métodos de la clase. Uno de los más importantes es
IsNullOrEmpty.
Este es uno de los métodos más usados a la hora de comparar strings y
ahora veremos porqué.
Imaginemos que nuestra variable string address no tiene valor. En C# no
podemos utilizar variables sin asignar así que voy a asignarle dos comillas
dobles, una al lado de la otra sin ningún espacio entre las comillas.

string address = "";

Eso es un string vacío y es equivalente a esto:

string address = string.Empty;

Ahora que tenemos una variable de tipo string vacía, declaramos otra
variable pero esta vez de tipo bool y agregamos una condición que va a
evaluar si address es una cadena vacía o no:

string number = "1";


string street = "Palo Alto St";
string city = "California";

string address = "";


bool isEmpty;

if (address == string.Empty)
isEmpty = true;
else
isEmpty = false;

Console.WriteLine(isEmpty);

Como acabamos de inicializar la variable address sabemos muy bien que


el resultado será verdadero, pero lo que nos interesa es la validación: si todo
sale bien devolverá verdadero, lo que quiere decir que la variable address ha
sido inicializada pero se le asignó un string vacío.
Ahora bien, las buenas prácticas dicen que la mejor manera de realizar
esta validación es utilizando el método IsNullOrEmpty, enviando la variable
por parámetros como sigue:

string number = "1";


string street = "Palo Alto St";
string city = "California";

string address = "";


bool isEmpty;

if (string.IsNullOrEmpty(address))
isEmpty = true;
else
isEmpty = false;

Console.WriteLine(isEmpty);

Como el resultado de la evaluación devolverá verdadero o falso,


realmente no necesito el if ya que con el valor devuelto por el método
obtendré lo mismo, así que podríamos resumirlo así:

string number = "1";


string street = "Palo Alto St";
string city = "California";

string address = "";

Console.WriteLine(string.IsNullOrEmpty(address));
El método IsNullOrEmpty además tiene la capacidad de evaluar si el
string es nulo:

string number = "1";


string street = "Palo Alto St";
string city = "California";

string address = null;

Console.WriteLine(string.IsNullOrEmpty(address));

En este ejemplo obtendremos el mismo resultado porque el método puede


evaluar, como dice su nombre, nulos o vacíos.

Un método similar es IsNullOrWhiteSpace, que abarca todo lo que ya


vimos (nulos y cadenas vacías) pero además también incluye al espacio, y
esto es particularmente importante cuando tenemos que evaluar cajas de texto
en las cuales el usuario puede haber agregado sin querer un espacio en blanco
pero en las cuales en realidad no hay nada.

string number = "1";


string street = "Palo Alto St";
string city = "California";

string address = " ";

Console.WriteLine(string.IsNullOrWhiteSpace(address));
String Builder

El tipo de datos string es inmutable por lo que no puede modificarse una


vez que ha sido creado. Claro que en la práctica podemos actualizar el valor
de cualquier variable de tipo string, solo que detrás de escena y sin que lo
veamos, el compilador creará un nuevo espacio de memoria para almacenar
el valor de la variable.

El problema es que si el string original es modificado demasiadas veces,


entonces la performance obviamente se verá comprometida. Para solucionar
este problema, C# agrega la clase StringBuilder.
StringBuilder no crea un nuevo objeto en memoria cada vez que el string
se modifica, sino que en cambio expande la memoria dinámicamente para ir
adaptándola al nuevo string, lo que permite a nuestra aplicación aprovechar
mucho mejor los recursos del sistema.

Un StringBuilder se inicializa como cualquier otra clase, usando la


palabra clave new.

StringBuilder sb = new StringBuilder();

Y puede ser inicializada sin pasar ningún valor por parámetros como en el
ejemplo, o también se puede usar uno de sus constructores predeterminados y
enviar el texto inicial de nuestra cadena:

StringBuilder sb = new StringBuilder(“Hi there!”);

Para devolver el valor del StringBuilder tendremos que usar el método


ToString del objeto:

string txt = sb.ToString();

Pero sin dudas, el uso más común de la clase StringBuilder es el


reemplazo directo de la concatenación y para esto contiene el método append,
que consiste en anexar una nueva cadena de texto a la que ya existe en el
objeto. Si no había ninguna cadena de texto en el objeto, entonces la
inicializará.

Veamos un ejemplo:

StringBuilder sb = new StringBuilder();

sb.Append(“Hi there!”);
sb.AppendLine (“How are you doing?”);

Aquí utilizamos primero Append y en la segunda línea AppendLine que


agrega la nueva cadena a la cadena existente en una nueva línea. El resultado
será este:

Hi there!
How are you doing?

StringBuilder también nos permite realizar otras operaciones con cadenas


de texto:

StringBuilder sb = new StringBuilder(“Hi Wick”);


sb.insert(2, “John”);

El método Insert permite agregar una cadena de texto en un índice


específico de la cadena (es decir, en una ubicación particular) y no al final de
la cadena.
El resultado aquí sería:

Hi John Wick

Se ha insertado la palabra John entre las palabras Hi y Wick.

También podemos remover cadenas de texto en cualquier lugar del string


con el método remove, indicando primero el índice a partir del cual comenzar
a contar y luego la cantidad de caracteres a remover:

StringBuilder sb = new StringBuilder(“Hi there!”);


sb.Remove(2,6);

En este ejemplo se removerán caracteres desde el índice 2 (recuerda que


en C# los índices comienzan desde cero), y luego seis lugares hacia adelante.
El resultado será este:

Hi

Se ha eliminado desde el espacio hasta la palabra there junto con el signo


de exclamación.

Otra de las operaciones más poderosas del StringBuilder es su método


Replace:

StringBuilder sb = new StringBuilder(“Hi there!”);


sb.Replace(“there”, “John”);

Lo bueno de este método es su simplicidad: simplemente tenemos que


pasar como primer parámetro la cadena que queremos reemplazar y, a
continuación, la cadena que la reemplazará.

En el ejemplo de arriba, el compilador buscará todas las apariciones de la


cadena there y reemplaza todas las que encuentre con la cadena John. Es
importante entender esto antes de usar el método Replace, porque si de
pronto la cadena de texto es muy larga y hay varias apariciones de esta
palabra, todas serán reemplazadas por el valor John.
En nuestro caso, el resultado será el siguiente:

Hi John
DateTime

En C#, para trabajar con fechas y tiempos es necesario crear una instancia
de la clase DateTime utilizando la palabra clave new como haríamos con
cualquier otra clase.

DateTime dt = new DateTime();

El menor valor posible que una variable DateTime puede tener es la del
primero de enero del año 1 a la medianoche:

Min value: 01/01/0001 00:00:00

Este es el valor por defecto de una variable DateTime que no ha sido


inicializada.
La variable dt que instanciamos arriba tendrá este valor ahora mismo.

El valor máximo posible de una variable DateTime es el 31 de diciembre


del año 9999 un segundo antes de la medianoche:

Max value: 12/31/9999 11:59:59

Es importante tener presentes estos valores por defecto porque serán muy
útiles a la hora de programar en C#.
DateTime, como muchas otras clases o estructuras, ofrece varios
constructores que podemos elegir según lo que necesitemos para inicializar
nuestra fecha.
En principio puede usarse con un constructor sin parámetros, en cuyo
caso tendrá por defecto el valor mínimo posible de los DateTime, pero
también podemos asignar año, mes y día a nuestra fecha para inicializarla
directamente en el día que necesitamos.

DateTime dt = new DateTime(2020, 12, 30);

También podemos agregar el componente del tiempo y pasar por


parametros horas, minutos y segundos a nuestra fecha.

DateTime dt = new DateTime(2020, 12, 31, 5, 10, 20);

Otra de las particularidades del tipo de datos DateTime es que incluye


campos y propiedades estáticas que podemos utilizar sin la necesidad de crear
un objeto.
Estos elementos son cruciales a la hora de trabajar con fechas porque nos
dan la posibilidad de tener valores de referencia para comparar con los
valores de nuestras variables.

Algunos ejemplos de estos campos estáticos son:

● DateTime.Now: fecha de hoy más la hora exacta en la que se invoca


el campo.
● DateTime.Today: fecha de hoy
● DateTime.Max: máximo valor posible
● DateTime.Min: mínimo valor posible.

Otra estructura relacionada con DateTime es Timespan que representa


tiempo en días, horas, minutos, segundos y milisegundos.
Timespan es sumamente útil para cuando necesitamos realizar
operaciones con fechas que agregan o remueven un valor de tiempo
determinado.

Supongamos que tenemos un DateTime con un valor particular (18 de


Agosto del año 2020):

DateTime dt = new DateTime(2020, 08, 18);

Y ahora supongamos que tenemos un Timespan (que no es otra cosa más


que una medida de tiempo), con el valor 25 horas, 20 minutos y 55 segundos

TimeSpan ts = new TimeSpan(25, 20, 55);

Ahora agreguemos el Timespan a nuestra fecha inicial utilizando el


método Add del objeto DateTime. El método Add sirve exclusivamente para
esto porque recibe por parámetros valores de tipo TimeSpan.
El valor resultante de la operación será una nueva fecha que
almacenaremos en otra variable:

DateTime newDate = dt .Add(ts);

Al imprimirla obtendremos lo siguiente:

19/08/2020 1:20:55 am

La fecha ha cambiado y ahora es 19 de agosto del 2020 a la una y veinte


de la mañana con cincuenta y cinco segundos. Este es el resultado de agregar
un valor de tiempo (TimeSpan) a la fecha original.

Otro uso importante del TimeSpan es el de funcionar como el tipo de


datos que resulta de una resta entre dos fechas. Es decir: cuando restamos una
fecha con otra, el resultado que obtenemos es un valor de tiempo.

Supongamos que tenemos dos fechas:

DateTime dt = new DateTime(2020, 08, 18);


DateTime dt2 = new DateTime(2021, 05, 07);

A la segunda fecha (la mayor de las dos) vamos a restarle la primera:

TimeSpan ts = dt2.Subtract(dt1);

Y al imprimir ts obtendremos la medida de tiempo que separa ambas


fechas:

262.00:00:00

Algunas lecciones atrás dijimos que cada tipo de datos reescribe los
operadores a su manera, es decir que no es lo mismo aplicar el operador de
suma a dos valores enteros que a dos cadenas de texto.
De la misma manera, el tipo de datos DateTime sobreescribe a los
operadores para que funcionen de una manera que tenga sentido para fechas.
Veamos algunos ejemplos de esto.

Supongamos que tenemos:


DateTime dt = new DateTime(2020, 08, 18);
DateTime dt2 = new DateTime(2021, 05, 07, 5, 10, 20);

TimeSpan ts = new TimeSpan(10, 5, 15, 50);

El TimeSpan ts representa una medida de tiempo de casi 10 días, así que


si le sumamos ese TimeSpan a la fecha inicial:

Console.WriteLine(dt2+ts);

Obtendremos esto:

17/05/2021 10:26:10 am

Y si ahora a dt2 le restamos dt:

Console.WriteLine(dt2-dt);

El resultado será:

262.05:10:20

Entonces realizar una diferencia entre dos fechas es equivalente a usar el


método Subtract que vimos arriba porque obtenemos un TimeSpan como
resultado.

También podemos igualar fechas o evaluar si dos fechas son iguales o no.

Console.WriteLine(dt2==dt);

Aquí el resultado será un valor booleano, en este caso falso porque las
fechas obviamente no son iguales.

Podemos determinar si dos fechas son distintas con el operador que ya


conocemos:

Console.WriteLine(dt2!=dt);

Y también podemos utilizar los operadores de comparación mayor y


menor:

Console.WriteLine(dt > dt2);


Console.WriteLine(dt < dt2);

Por si todo esto fuera poco, DateTime nos ofrece una serie de métodos
para convertir la fecha a una cadena de texto utilizando diversos formatos
según la información que necesitemos mostrar. Esto es muy útil para mostrar
la fecha por pantalla al usuario.

Method Description Example


Depends on selected 18/08/2020 12:00:00
ToString
culture am
ToShortDateString M/d/yyyy 18/08/2020
ToShortTimeString h:mm:ss 12:00 am
Tuesday,18 August
ToLongDateString dddd, MMMM d,yyyy
2020
ToLongTimeString H:mm:ss tt 12:00:00 am

El método ToString convierte la fecha a un valor de tipo string según el


formato especificado por la cultura. Incluye fecha y hora.
El segundo método ToShortDateString, excluye el factor tiempo y solo
mostrará la fecha en formato numérico: número para día, número para mes y
número para año.
El tercer método ToShortTimeString solo imprime el factor tiempo
olvidándose por completo de la fecha, y en ToLongDateString tampoco
aparecerá el tiempo, solo la fecha pero con un formato mucho más amplio
con día, mes completo y el número del año.
Finalmente ToLongTimeString imprime sólo el tiempo, incluyendo hora,
minutos y segundos.
Tipos de Variable
Variables De Tipo Implícito

En los primeros capítulos de este libro dijimos que en C# las variables


deben ser declaradas con su tipo de datos.

int i = 100;

Esto es lo que llamamos variables de tipo explícito, justamente porque


estamos haciendo explícito cuál es el tipo de la variable. Una alternativa que
C# ofrece es la de utilizar la palabra clave var para declarar variables dentro
de métodos sin la necesidad de hacer explícito cuál es el tipo de datos de la
variable.

var x = 100;

Esto es lo que se conoce como variable de tipo implícito. En este tipo de


declaraciones el compilador infiere cuál es el tipo de la variable según la
expresión que está a la derecha del signo igual (=).
En el ejemplo de arriba la variable x compilara como un número entero, al
igual que la variable y a continuación:

var y = i + 20;

Como el resultado de la expresión que está a la derecha del signo igual es


un entero, entonces la variable y también será de tipo entero.
El uso de la palabra clave var no se reduce a tipos de datos primitivos
sino que también puede ser usado con variables de tipo string, DateTime y
con cualquier tipo de datos definido por el usuario.

var s = “Hello World”;


var car = new Car();

Las variables de tipo implícito no tienen ninguna limitación en cuanto al


tipo de datos que puede ser utilizado.

Algunas consideraciones a tener en cuenta con variables de tipo implícito:


● Siempre deben ser inicializadas ya que el tipo de la variable es
inferido a partir de la inicialización.
● No pueden ser utilizadas como parámetros de funciones
● Sí pueden ser utilizadas en bucles for y for each.
Tipos por Valor y por Referencia

En C# los tipos de datos se pueden categorizar según cómo almacenan


sus valores en memoria. Pueden ser por valor o por referencia.

Por valor Por referencia


(Value Type) (Reference Type)

Se dice que un tipo de datos es un tipo por valor si almacena el valor en


su propio espacio de memoria, por ejemplo, si tenemos una variable i de tipo
int cuyo valor es 100 entonces el sistema almacenará el valor 100 en el
espacio de memoria asignado para i que, hipotéticamente, diremos que está
en la dirección 0x228110.
Los más comunes de los tipos de datos por valor son bool, byte, char,
decimal, double, int, float y long.

La diferencia principal entre tipos de datos por valor y tipos de datos por
referencia aparece a la hora de pasar valores por parámetro. Cuando pasamos
una variable de tipo de datos por valor de un método a otro, el sistema crea
una copia de la variable en el segundo método. Esto significa que si el valor
de la variable cambia dentro de uno de los métodos esto no afecta el valor de
la variable en el otro método.

Veamos un ejemplo:

static void IncrementValue(int n)


{
n = 20
Console.WriteLine(n);
};

static void SetValue()


{
Int n = 10;
Console.WriteLine(n);
IncrementValue(n);
Console.WriteLine(n);
};

Primero tenemos un método IncrementValue que recibe por parámetros


una variable de tipo int y luego actualiza el valor de ese parámetro a 20 sin
importar el valor previo que tenga la variable.
A continuación tenemos un método SetValue que se ejecuta antes que el
método IncrementValue y que, en medio del proceso, lo invoca y le pasa el
valor de n por parámetros.
Lo que veremos por pantalla al ejecutar el código será esto:

10
20
10

Analicemos el orden de la secuencia: primero se declara la variable n y se


la inicializa con un valor de 10; ese es el primer valor que imprimimos en
consola. Luego se llama al método IncrementValue y se le pasa por
parámetros el valor de n que sabemos que vale 10 pero inmediatamente se
actualiza a 20 dentro del mismo método IncrementValue y se imprime por
consola como 20. Como dijimos que los tipos de datos por valor crean una
nueva copia del valor de la variable, la variable n del método IncrementValue
técnicamente no es la misma que la variable n del método SetValue ya que
son espacios de memoria diferentes. Por eso, cuando imprimimos este último
valor en consola lo que vemos es 10 ya que el espacio de memoria asignado a
n dentro del método SetValue no ha cambiado realmente. En este caso se
estará imprimiendo la variable n local al método Setvalue que nunca ha
cambiado y que nada tiene que ver con la variable n local el método
IncrementValue que está en un espacio de memoria completamente diferente.

A diferencia de los tipos por valor, un tipo por referencia no almacena el


valor directamente sino que almacena la dirección de memoria en donde el
valor está efectivamente almacenado. En otras palabras, un tipo de dato por
referencia contiene un puntero a otro espacio de memoria, que es el que tiene
realmente el valor.

Pensemos en una variable de tipo string, que es un ejemplo de un tipo de


datos por referencia:

string s = “Hi”;

Repasemos cómo funciona la asignación de memoria aquí: cuando la


variable s es inicializada, el sistema selecciona aleatoriamente una dirección
de memoria y el tipo por referencia almacena la dirección de memoria en
donde el valor se almacena, pero no el valor en sí mismo. Los tipos de datos
por referencia más comunes son string, arrays y class, es decir, cualquier
clase que necesitemos, y además delegate, que son delegados.
Ahora veamos cómo se comporta una variable de tipo de dato por
referencia cuando es pasada como argumento de otro método. Igual que antes
imaginemos dos métodos: UpdateValue que recibe por parámetros una
variable de tipo Contact que es una clase de nuestro sistema. Este método lo
único que hace es actualizar el nombre del contacto a “Alan”.
Y también pensemos en un método SetValue que se ejecuta antes que
UpdateValue y que declara e inicializa la variable c de tipo Contact y le
asigna “Mark” como su nombre.
Luego de eso, llama al método UpdateValue en donde la actualización del
nombre sucede.
Y por último, imprime el nombre del contacto por consola.

static void UpdateValue(Contact con)


{
con.Name = “Alan”;
};

static void SetValue()


{
Contac c = new Contact();
c.Name = “Mark”;
UpdateValue(c);
Console.WriteLine(c.Name);
};

Lo que obtendremos por consola será:

Alan

A diferencia de lo que sucedía antes, ahora la variable que se ha


modificado en el segundo método sí que ha cambiado el valor de la variable
del método SetValue cuando consultamos su valor. Esto se debe a que cuando
la variable es enviada por parámetros no se crea una copia del espacio de
memoria, sino que lo que se pasa por parámetros es la dirección de memoria
en donde está el valor realmente.
Por lo cual ambas variables, tanto la local all método UpdateValue como
la local el método SetValue están apuntando al mismo espacio de memoria y
por consiguiente, modificando el verdadero y único valor de la variable.
Estructuras de Control
IF/Else

Una de las estructuras de control más importantes en todo el lenguaje de


programación es la sentencia IF. Si ya has usado esta sentencia en otro
lenguaje, entonces podrás comenzar a usarla en C# sin mucho preámbulo.

La sentencia IF necesita de un resultado booleano que puede ser


verdadero o falso:

if (condition)
{
//code to run when condition == true
}

Esto es lo que se conoce como condición. La condición será evaluada en


tiempo de ejecución y si es verdadera, entonces el bloque de código que está
entre llaves será ejecutado. Si la condición es falsa, entonces no será
ejecutado.

Consideremos dos variables enteras con dos valores por defecto y una
sentencia IF:

int x = 100, y = 200;

if (x < y)
{
Console.WriteLine(“x is lesser than y”);
}

En el ejemplo, la condición dice que x tiene que ser menor que y para que
el código del IF se ejecute. Evidentemente cuando la condición sea evaluada,
su resultado será verdadero y por lo tanto el código que está dentro del
bloque IF será ejecutado.

Pero consideremos el caso contrario:


if (x > y)
{
Console.WriteLine(“x is greater than y”);
}

Aquí la condición es que x sea mayor que y, lo cual no es cierto como


podemos ver en la declaración de las variables, por lo tanto la evaluación de
la condición dará falso y el código no será ejecutado.

int x = 100, y = 200;

Estas mismas sentencias if también se pueden escribir así:

if (x < y)
Console.WriteLine(“x is lesser than y”)
if (x > y)
Console.WriteLine(“x is greater than y”)

La única diferencia respecto a lo que vimos recién es que no hay llaves


después de las condiciones de los IF, esto es porque no necesitamos utilizar
las llaves si el bloque de código del IF contiene una sola línea de código.
Ahora tomemos un ejemplo muy similar, pero eliminemos la segunda
variable para simplificar la demostración.

int x = 100;

Ahora agregamos una sentencia if, aunque esta vez la condición será
levemente diferente:

if (x > 200)
Console.WriteLine(“x is greater than 200”);

Esta vez la evaluación verifica si x es mayor a 200 o no; si no es mayor a


200, entonces el bloque de código que le sigue no se ejecutará.
Como bien sabemos, cuando esta condición se evalúa en tiempo de
ejecución el resultado efectivamente es falso, por lo cual se puede aprovechar
la oportunidad para agregar justo debajo del bloque de código una condición
else.
if (x > 200)
Console.WriteLine(“x is greater than 200”);
else
Console.WriteLine(“x is lesser than 200”);

El bloque de código que está en la sentencia else será ejecutado sólo si el


resultado de la condición del if es falso.
Este es el ejemplo clásico de un condicional if/else.

Lo último que tenemos que saber del condicional IF es que soporta


múltiples sentencias else, de modo tal que cuando la primera condición sea
falsa, entonces la secuencia pasará a evaluar la condición del segundo if, si
está también es falsa entonces ejecutará lo que está en el else:

if (x > 200)
Console.WriteLine(“x is greater than 200”);
else if (x > 100)
Console.WriteLine(“x is greater than 100”);
else
Console.WriteLine(“x is lesser than 100”);

Esto nos permite crear una cadena de condiciones para cuando los
resultados de las evaluaciones son sucesivamente falsos. No hay un límite en
cuanto a la cantidad de else if que podemos tener dentro de un condicional if,
aunque sí es importante decir que si tenemos muchos es probable que exista
otra estructura de control que se ajuste mejor a lo que necesitamos.
Operador Ternario

Además de la estructura If/else convencional, C# nos permite utilizar el


operador ternario que es básicamente un if/else clásico pero en en el formato
más comprimido posible.

condition ? statement : statement2;

El operador ternario comienza con una condición booleana tal cual sería
en un IF. Si la evaluación de esta condición es verdadera, entonces se
ejecutará el primer statement que sigue al signo de preguntas.
Si en cambio la evaluación de la condición es falsa, entonces se ejecutará
el segundo statement que está después de los dos puntos.

condition ? statement : statement2;

int x = 10, y = 5;
string z = x > y ? “greater x” : “greater y”;

En el ejemplo aparecen dos variables numéricas x e y. Luego en el


operador ternario aparece primero una condición x mayor a y, y a
continuación la primera sentencia devuelve una cadena de texto que dice que
x es mayor mientras que la segunda sentencia dice que y es mayor. La forma
en la que se lee esta condición es: si x es mayor a y, entonces almacenar en la
variable z la cadena “greater x”. Si no, si en cambio x no es mayor a y,
entonces almacenar en la variable z la cadena “greater y”.
Al ser equivalente al if/else tradicional también lo podríamos escribir de
esta manera, pero fíjate cómo ahorramos palabras y espacio utilizando el
operador ternario.
Los operadores ternarios, así como las estructuras if/else convencionales,
soportan anidados, o lo que es lo mismo, podemos agregar un operador
ternario dentro de otro:

int x = 10, y = 5;

string z = x > y
? “x is greater than y”
: x < y ? “y is greater than x”
: x == y ? “x is equal to y”
: “How did you get here”;

Aquí tenemos un ejemplo parecido al anterior.


Si prestamos especial atención a las condiciones, tenemos primero la
misma que antes: si x es mayor a y entonces z tendrá el valor “x is greater
than y”, si x no es mayor a y,entonces llegaremos a la segunda condición que
pregunta si x es menor a y, si esto es verdadero, entonces z tendrá el valor “y
is greater than x”. Pero si esta segunda condición es falsa, entonces una
nueva condición será evaluada: si x es igual a y, entonces z tendrá el valor “x
is equal to y”. La última salida de este operador ternario es la cadena “how
did you get here”, porque es prácticamente imposible que esta opción sea
alcanzada.
Como puedes ver, un operador ternario puede ser tan complejo como sea
necesario.
Lo importante aquí es que el código sea legible y que las condiciones
tengan sentido para que el código quede limpio y se entienda fácilmente. Si el
operador ternario se vuelve demasiado complejo, entonces es probable que la
mejor solución sea otra estructura de control.
Práctico

Vamos a analizar algunos ejemplos de la estructura de control más simple


del lenguaje y que además es fundamental en cualquier lenguaje de
programación.
Comencemos con una variable de tipo string y una de tipo booleano.

string CustomerName = "Orson Welles";


bool IsJohn;

if (CustomerName == "John Travolta")


IsJohn = true;
else
IsJohn = false;

Console.WriteLine(IsJohn);

A continuación haré una validación utilizando un if y dentro de la


condición utilizaré los operadores que vimos al principio del curso. Cualquier
expresión está permitida dentro del if siempre y cuando su evaluación (es
decir el resultado luego de ser evaluada por el compilador) sea de tipo
booleano.
Entonces, si la condición es verdadera, el primer bloque de código será
ejecutado, si el resultado de la condición es falsa, entonces el segundo bloque
de código será el que se ejecute.
false

Como es de esperar, el resultado aquí es falso, pero qué pasa si


cambiamos la expresión y ahora en lugar de igual, preguntamos si los valores
son distintos.

string CustomerName = "Orson Welles";


bool IsJohn;

if (CustomerName != "John Travolta")


IsJohn = true;
else
IsJohn = false;

Console.WriteLine(IsJohn);

Ahora el resultado cambia:

True

La evaluación de la condición if devolverá siempre un valor booleano y


en base a ese valor decidirá cuál es la secuencia que debe continuar en el
programa, por lo tanto, tiene sentido que podamos utilizar como condición
directamente una variable de tipo booleano. Por lo que no habría problemas
en agregar algo como esto:

string CustomerName = "Orson Welles";


bool IsJohn;

if (CustomerName != "John Travolta")


IsJohn = true;
else
IsJohn = false;

if(IsJohn)
Console.WriteLine("IsJohn es verdadero");
else
Console.WriteLine("IsJohn es falso");

El resultado de ejecutar esta porción de código será:

IsJohn es verdadero

Supongamos que volvemos al primer ejemplo en donde la evaluación de


la condición es falsa. Otra expresión que podríamos usar aquí y que de hecho
verás muy seguido en C# es la negación de una variable booleana:

string CustomerName = "Orson Welles";


bool IsJohn;
if (CustomerName == "John Travolta")
IsJohn = true;
else
IsJohn = false;

if(!IsJohn)
Console.WriteLine("IsJohn es verdadero");
else
Console.WriteLine("IsJohn es falso");

Usando el signo de exclamación negamos el valor booleano, por lo tanto


si imprimimos ahora vamos a obtener lo mismo que antes:

IsJohn es verdadero

En definitiva, podemos usar en nuestra condición cualquier expresión que


se evalúe como valor booleano.

Vamos a olvidarnos de las cadenas de texto por ahora y vamos a crear dos
variables enteras. El bloque de código contenido en el if puede contener
tantas líneas como sea necesario, igual que el else en donde podríamos
incluso anidar estructuras if si quisiéramos:

int x = 15;
int y = 30;

if (x > y)
{
Console.WriteLine("x es mayor a y");
Console.WriteLine("ademas podemos tener mas de una linea");
Console.WriteLine("y mas de dos");

}
else
{
Console.WriteLine("x es menor a y");
Console.WriteLine("ademas podemos tener mas de una linea");
Console.WriteLine("y mas de dos");
}

Al Imprimir esto veremos en pantalla tres líneas y, según la evaluación de


la condición se ejecutará el segundo bloque de código por que x es menor a y.
Como se puede ver, la estructura if es bastante sencilla. Digamos que es la
estructura de control fundacional en cualquier lenguaje de programación y es
lo que nos permite crear lógica y programar nuestra aplicación para que haga
una cosa u otra según el valor de las variables o la evaluación de ciertas
expresiones.
Para ahorrar un poco de código aquí podríamos utilizar el operador
ternario; mientras las dos caminos devuelvan el tipo de datos que tienen que
devolver o que el compilador está esperando, entonces no hay problema en
cuándo utilizar el operador ternario. Obviamente, en estos casos no puede
haber más de una línea en el cuerpo del if o del else dada la simplicidad del
operador.

int x = 15;
int y = 30;

Console.WriteLine(x > y ? "x es mayor a y" : "y es mayor a x");


Switch

Cuando tenemos que evaluar una variable con dos o más condiciones, en
lugar de usar un gran if con muchos else, podemos utilizar la sentencia
switch.

int x = 10;
switch (x)
{
case 0:
Console.WriteLine(“x is 0”);
break;
case 10:
Console.WriteLine(“x is 10”);
break;
default:
Console.WriteLine(“x is not 0 nor 10”);
break;
}

Esta sentencia comienza con la palabra clave switch y puede contener una
variable (como en este caso) o también una expresión, siempre que el tipo de
datos resultado de la expresión sea char, string, bool, int o enum.

El resultado de esta expresión (o el valor de la variable) dentro del switch


será comparado con cada uno de los valores especificados en los cases. Algo
importante a tener en cuenta es que un case debe ser definido con un valor
constante, es decir que no puede tener un valor variable. Además, cada
instrucción case debe terminar con dos puntos y estar seguido de una o más
líneas que serán ejecutadas cuando esa condición sea verdadera.
En otras palabras: estas líneas serán ejecutadas sólo si el valor constante
del case y el valor variable del switch coinciden.
En el ejemplo de esta sección tenemos una variable x que es igual a 10;
dentro del switch se evalúa esa variable y, como su valor es 10, la única línea
que se ejecutará es la que está dentro del case 10.
Toda cláusula switch tiene una instrucción default que es la que se puede
ver al final del ejemplo, abajo de todo. Esta cláusula es opcional y se utiliza
para especificar una salida para cuando la evaluación del switch no coincide
con ninguno de los valores de los case.
En este ejemplo si x no fuera ni 10 ni 0, no coincidiría con ninguna de las
constantes de los case por lo que el código del default sería ejecutado. A su
vez, cada uno de los case tiene una instrucción break que es una palabra
clave obligatoria en todo case y que sirve para delimitar el final del bloque de
código contenido en el case. Alternativamente a la palabra clave break se
puede usar la palabra clave return para retomar la ejecución.

Algo importante a aclarar es que en cuanto la ejecución de la aplicación


entra a uno de los cases, ningún otro será ejecutado y el flujo de ejecución
pasará a la línea de código que siga después del switch.

La cláusula switch también permite combinar cases para ejecutar una


misma porción de código según el resultado de la evaluación:

int x = 10;
switch (x)
{
case 0:
case 1:
Console.WriteLine(“x is 0 or 1 “);
break;
case 10:
case 11:
Console.WriteLine(“x is 10 or 11 “);
break;
default:
Console.WriteLine(“x is not in 0,1,10,11“);
break;
}

En este ejemplo, cuando x sea 0 o 1 el primer case será ejecutado, y


cuando sea 10 u 11, entonces el segundo case será ejecutado. Si ninguno de
los cases coincide con el valor de x, entonces el código del default será
ejecutado.
Práctico

La sentencia switch generalmente se usa cuando queremos evaluar


nuestra variable o expresión contra tres o más condiciones. Es una buena
práctica utilizar un switch en lugar de un if siempre que detectemos que
nuestra sentencia if es demasiado compleja y tiene muchas ramas.

Empecemos con un primer ejemplo y después vamos a explorar las


particularidades de la sentencia:

using System;

namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Do you like C# so far? (yes / no /maybe)");
string input = Console.ReadLine();

switch (input.ToLower())
{
case "yes":
Console.WriteLine("That is great");
break;

default:
break;
}
}
}
}

En este ejemplo le pedimos al usuario que interactúe con la consola. Por


pantalla aparecerá la pregunta "Do you like C# so far? (yes / no /maybe)" y a
continuación la aplicación se quedará esperando que el usuario escriba algo y
capturará esa respuesta en el string input.
Luego aparece la sentencia switch, en donde se convierte a minúsculas lo
que el usuario ha escrito con el método “ToLower”, de este modo nos
aseguramos que por más que el usuario haya escrito todo en mayúsculas el
case “yes” igual podrá ser igualado.
Este es un método de la clase string que convierte todos los caracteres de
esta cadena de texto a minúsculas.
Entonces cuando el valor de input convertido a minúsculas sea yes,
entonces se ejecutará el bloque de código del case en donde se imprime por
pantalla el texto ”that is great”.
El break es el indicador de que este bloque de código ha terminado,
porque obviamente aquí podríamos tener múltiples líneas a ejecutar.

Ese es un ejemplo bien simple, pero puedo obviamente escribir múltiples


cases:

using System;

namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Do you like C# so far? (yes / no /maybe)");
string input = Console.ReadLine();

switch (input.ToLower())
{
case "yes":
Console.WriteLine("That is great");
break;
case "maybe":
Console.WriteLine("It'll get better");
break;
case "no":
Console.WriteLine("That is not so good");
break;

default:
break;
}
}
}
}

Con este agregado he cubierto las tres alternativas que le estoy dando al
usuario como posibilidades para que responda. Ahora bien, si el usuario
responde cualquier cosa que no sea ninguna de estas tres opciones, entonces
se va a ejecutar el default.
Si bien el default es opcional, en este caso me interesa que exista así que
voy a agregar algo más significativo.

using System;

namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Do you like C# so far? (yes / no /maybe)");
string input = Console.ReadLine();

switch (input.ToLower())
{
case "yes":
Console.WriteLine("That is great");
break;
case "maybe":
Console.WriteLine("It'll get better");
break;
case "no":
Console.WriteLine("That is not so good");
break;

default:
Console.WriteLine("Wasn't an option");
break;
}
}
}
}

Esta que vimos es la forma más clásica del switch. Ahora voy a retocar un
poco el código de arriba para usar el switch exclusivamente con enteros:

using System;

namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
int x = 10;

switch (x)
{
case 5:
Console.WriteLine("Value of x Is 5");
break;
case 10:
Console.WriteLine("Value of x Is 10");
break;
case 15:
Console.WriteLine("Value of x Is 15");
break;

default:
Console.WriteLine("Unknown value");
break;
}
}
}
}

En esencia la estructura switch realmente no ha cambiado.

Algo que podemos agregar es una expresión en lugar de un valor en el


cuerpo del switch:

using System;

namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
int x = 10;

switch (x + 5)
{
case 5:
Console.WriteLine("Value of x Is 5");
break;
case 10:
Console.WriteLine("Value of x Is 10");
break;
case 15:
Console.WriteLine("Value of x Is 15");
break;

default:
Console.WriteLine("Unknown value");
break;
}
}
}
}

Ahora hay una operación que se ejecutará y a partir del resultado se


evaluarán los valores constantes de los case.
También podemos agrupar los cases para que determinado bloque de
código se ejecute cuando el resultado de la condición coincida con al menos
uno de los cases agrupados. Con esto conseguiremos que la estructura sea un
poco más flexible:

using System;

namespace ConsoleApp9
{
class Program
{
static void Main(string[] args)
{
int x = 10;

switch (x + 5)
{
case 1:
case 2:
case 3:
case 4:
case 5:
Console.WriteLine("Value of x Is < 5");
break;
case 10:
Console.WriteLine("Value of x Is 10");
break;
case 15:
Console.WriteLine("Value of x Is 15");
break;

default:
Console.WriteLine("Unknown value");
break;
}
}
}
}

O también podríamos intentar abarcar más posibilidades simplemente


agregando más cases:

using System;

namespace ConsoleApp9
{
class Program
{
static void Main(string[] args)
{
int x = 10;

switch (x + 5)
{
case 1:
case 2:
case 3:
case 4:
case 5:
Console.WriteLine("Value of x Is < 5");
break;
case 6:
case 7:
case 8:
case 9:
case 10:
Console.WriteLine("Value of x Is <= 10, but > 5");
break;
case 11:
case 12:
case 13:
case 14:
case 15:
Console.WriteLine("Value of x Is <= 15, but > 10");
break;

default:
Console.WriteLine("Unknown value");
break;
}
}
}
}

Con esto ya se puede observar la complejidad que puede llegar a tomar


una de estas estructuras de control. Esta que acabamos de ver quizás no sea la
mejor forma de escribir esa lógica de negocio, simplemente lo agregué de
este modo para poder demostrar cómo usar el switch y sus varias
combinaciones.
For
En C# el loop FOR funciona exactamente igual que en otros lenguajes.

for (initializer; condition; iterator;)


{
//code
}

La palabra clave for designa el comienzo del loop y la idea de la


estructura es que el bloque de código se ejecute repetidas veces hasta que la
condición especificada sea falsa.
Contiene tres secciones separadas por punto y coma:
● El initializer o el inicializador es utilizado para inicializar la variable
que será local dentro del loop y no será accesible por fuera de este.
● La condición es una expresión booleana que devolverá verdadero
falso al ser evaluada. Si la evaluación de la expresión es verdadera
entonces el bloque de código for será ejecutado otra vez; si es falsa
entonces no se ejecutará de nuevo el bloque de código y las
iteraciones se darán por terminadas saliendo del bucle.
● Por último iterator o iterador define el incremento o decremento de
la variable del loop que inicializamos en el initializer.

Este es el ejemplo de una estructura for:

for (int i = 0; i < 10; i++)


{
Console.Writeline($”Value: {i}”);
}

En el inicializado tenemos la declaración e inicialización de una variable i


de tipo int, que tiene un valor inicial igual a cero.
En la segunda sección, que es la de la condición, aparece una expresión
simple que verifica si el valor de la variable i es menor a 10. Si esta condición
es verdadera, entonces el bloque de código será ejecutado, y una vez
ejecutado el bloque de código el proceso continuará con la tercera sección del
foro: el iterador i++. Esta es una sentencia incremental como ya vimos en la
sección de operadores, que aumentará el valor de la variable i en una (1)
unidad. Ahora el proceso verificará la expresión condicional y repetirá el
proceso hasta que la expresión condicional sea falsa.
En el ejemplo, el bloque de código se ejecutará 10 veces (de 0 a 9 para la
variable i).
Al igual que en la sentencia if else, si el bloque de código contiene una
sola sentencia como en este caso, entonces no sería necesario utilizar llaves.
Práctico

La sentencia for es una de las más usadas en C# (y en todo lenguaje de


programación realmente) porque resulta imprescindible para alterar
elementos en una colección, y cómo ya veremos también, la gran mayoría de
los elementos tienden a formar parte de una colección tarde o temprano.

for (int i = 0; i < 20; i++)


{
Console.WriteLine($"Value of i is: {i}");
}

Al ejecutar ese bucle for obtendremos algo como esto:

Value of i is: 0
Value of i is: 1
Value of i is: 2
Value of i is: 3
Value of i is: 4
Value of i is: 5
Value of i is: 6
Value of i is: 7
Value of i is: 8
Value of i is: 9
Value of i is: 10
Value of i is: 11
Value of i is: 12
Value of i is: 13
Value of i is: 14
Value of i is: 15
Value of i is: 16
Value of i is: 17
Value of i is: 18
Value of i is: 19

Esto ejemplifica bastante bien como funciona un bucle for en C#.


El bloque de código se ha ejecutado 20 veces y en cada iteración el valor
de i fue incrementando de uno en uno hasta llegar a diecinueve; como el
valor inicial era 0, el último valor es 19.
En esencia un for es un iterador que ejecuta un bloque de código tantas
veces como le indiquemos.

El que acabamos de ver es un ejemplo clásico de un bucle for en donde la


sección del iterador incrementa de uno en uno, pero también podría ser al
revés y el iterador en realidad ser un decremento de uno en uno:

for (int i = 10; i > 0; i--)


Console.WriteLine($"Value of i is: {i}");

Ahora obtendremos esto por consola:

Value of i is: 10
Value of i is: 9
Value of i is: 8
Value of i is: 7
Value of i is: 6
Value of i is: 5
Value of i is: 4
Value of i is: 3
Value of i is: 2
Value of i is: 1

Esto es lo que se llama un reverse for loop o un loop for a la inversa.


El valor de i comienza en 10 y en cada iteración se le resta una unidad
hasta llegar a 1, en donde el bucle termina porque no se cumple la condición
de que i sea mayor que 0.

Pero volvamos al ejemplo anterior que es un poco más convencional.


Dentro de un bucle for también se pueden agregar secuencias que nos
permitan salir del loop. Un ejemplo de esto es la utilización de la palabra
clave break;

for (int i = 0; i < 20; i++)


{
if (i == 4)
break;

Console.WriteLine($"Value of i is: {i}");


}

En el ejemplo, el bucle for itera cinco veces entre 0 y 4, y cuando


finalmente llega a 4 la secuencia entra al if porque la condición se cumple, se
ejecuta la sentencia break y el loop termina. Esto puede ser útil cuando
iteramos una colección muy grande en la que no sabemos en dónde está el
valor que estamos buscando. La palabra clave break nos permite forzar la
finalización del loop una vez que se encuentre el valor para liberar este
proceso de memoria y pasar al siguiente.

El último ejemplo que veremos de esta estructura de control, es la de dos


for anidados:

for (int i = 0; i < 5; i++)


{
for (int j = i; j < 5; j++)
{
Console.WriteLine($"i: {i}, j: {j}");
}
}

Este será el resultado:

i: 0, j: 0
i: 0, j: 1
i: 0, j: 2
i: 0, j: 3
i: 0, j: 4
i: 1, j: 1
i: 1, j: 2
i: 1, j: 3
i: 1, j: 4
i: 2, j: 2
i: 2, j: 3
i: 2, j: 4
i: 3, j: 3
i: 3, j: 4
i: 4, j: 4

Como puedes ver, analizar el resultado es más fácil que explicarlo: por
cada valor de la variable i se ejecuta un bucle que que recorre los valores de
la variable j y por cada una de estas iteraciones imprime el valor de ambas
variables por consola
Las buenas prácticas dicen que siempre que podamos evitar los loops
anidados debemos hacerlo; aunque también es cierto que hay circunstancias
en las que esto no es posible. Para estos casos la herramienta existe y
podemos utilizarla sin problemas.
Más de dos loops anidados seguramente significa que podemos encontrar
una solución mejor.
While

Cuando necesitamos ejecutar un bloque de código repetidamente mientras


la evaluación de la condición sea verdadera, usaremos la estructura while.

while (condition)
{
//code block
}

El loop while comienza con la palabra clave while y debe incluir una
expresión condicional booleana que devuelva verdadero o falso. El loop
ejecutará el bloque de código hasta que la expresión condicional sea falsa.

int i = 0;

while (i < 20)


{
Console.WriteLine($”Value: {i}”);
i++;
}

A diferencia del for en donde la inicialización y el incremento o


decremento de la variable se escribe en la misma definición de la sentencia,
en el while la inicialización de la variable debe suceder antes de que el loop
empiece y el incremento o decremento de la variable debe ocurrir dentro del
loop.
En el ejemplo, el bucle while incluye una expresión que valida si el valor
de la variable i es menor a 20; mientras esto sea verdadero se ejecutará el
bloque de código y además la variable i será incrementada. Luego se valida
una vez más si la condición es verdadera y así se repite hasta que la condición
sea falsa.
En el ejemplo, el bloque de código se ejecutará 20 veces entre valores de
cero y diecinueve para i.
Práctico

Como su nombre lo indica, la estructura condicional while ejecutará un


determinado bloque de código mientras una cierta condición sea verdadera:

while (true)
{
//Code to be executed
}

Como se puede ver, es una estructura sencilla y sin gran complejidad en


cuanto a su sintaxis ya que tiene sólo tres elementos: inicialización, condición
e incremento.

int i = 0; //initialization

while (i < 10) //condition


{
Console.WriteLine($"The value of i is: {i}");
i++; //increment
}

Supongamos una variable int i igual a 0, el while de ejemplo se lee:


“mientras i sea menor a 10, entonces imprimimos el valor de la variable y
luego incrementamos el valor de la variable en 1 unidad”.
Todo while debe tener una inicialización, una condición que se
compruebe verdadera y siempre que sea verdadera lo que esté dentro de las
llaves se va ejecutar. Luego tiene que haber un incremento para que, cuando
la condición se vuelva a evaluar, el valor de la variable haya cambiado.

Lo que obtenemos aquí es lo siguiente:

The value of i is: 0


The value of i is: 1
The value of i is: 2
The value of i is: 3
The value of i is: 4
The value of i is: 5
The value of i is: 6
The value of i is: 7
The value of i is: 8
The value of i is: 9

Así como era para el for, en la estructura while también tenemos la


opción de finalizar o de salir del bucle cuando cierta condición se cumpla:

int i = 0; //initialization

while (i < 10) //condition


{
if (i == 8)
break;

Console.WriteLine($"The value of i is: {i}");


i++; //increment
}

Aquí está claro que, una vez que el valor de la variable sea 8, el bucle
terminará ya que aparece la palabra clave break.

The value of i is: 0


The value of i is: 1
The value of i is: 2
The value of i is: 3
The value of i is: 4
The value of i is: 5
The value of i is: 6
The value of i is: 7

Con la estructura while hay que tener cuidado porque, a diferencia quizás
de otras estructuras, puede convertirse con cierta facilidad en un loop infinito,
es decir, en un loop que no se termina nunca. Esto obviamente es peligroso
en una aplicación porque puede tener un costo muy alto en cuanto a la
performance y en cuanto a que obviamente en algún momento se va a
consumir toda la memoria de la aplicación.
El ejemplo más conocido de esto es el famoso while true:

while (true)
{
Console.WriteLine($"Infinite loop");
}

Al ejecutar esto las palabras “infinite loop” se repetirá infinitamente sin


ningún tipo de límite. Esto es obviamente un while true explícito. Cuando se
escribe un while true, también se escribe dentro del cuerpo del while una
condición acompañada de un break para que el bucle en algún momento se
corte y finalice.

Pero también puede ocurrir que un loop infinito como éste, suceda de
manera involuntaria:

int i = 0;

while (i >= 0)
{
Console.WriteLine($"i = {i}");
i++;
}

Si prestamos atención a la condición podremos ver que siempre será


verdadera y que además no hay dentro del bloque código nada que modifique
a la variable de forma tal que la condición alguna vez sea falsa. Por lo cual
este código se ejecutará indefinidamente hasta que la aplicación se quedé
memoria.

Igual que en el resto de las estructuras, también podemos anidar whiles,


colocando uno dentro de otro:

int i = 0;
int j = 1;

while (i < 5)
{
Console.WriteLine($"i = {i}");
i++;

while (j < 5)
{
Console.WriteLine($"j = {j}");
j++;
}
}

Esto es lo que veremos por pantalla:

i=0
j=1
j=2
j=3
j=4
i=1
i=2
i=3
i=4

Los valores de i van de cero hasta a cuatro. Para el primer loop se ejecuta
el segundo while en donde j es menor a cinco, y luego, como j ya tiene el
valor 5, no se vuelve a ejecutar por lo que no aparecen más resultados y sólo
vemos impresiones de i.

De nuevo aquí es importante aclarar que, en general, anidar estructuras de


control no es la mejor práctica. Si en algún momento surge la necesidad de
anidar dos whiles, hay muchas probabilidades de que haya una mejor
solución para ese problema.
Do While

El loop do while funciona de la misma manera que el while con la única


excepción de que en este caso el bloque de código se ejecuta al menos una
vez:

do
{
//code block
}
while (condition)

En el while la condición se evaluaba al principio y luego, si el resultado


era verdadero, se ejecutaba el bloque de código. Claro que en ese caso existe
la posibilidad de que el resultado de la evaluación sea falso y el bloque de
código nunca se ejecute; esa es la única diferencia que el do while impone.
Siempre se ejecutará el bloque de código y luego dependiendo de la
evaluación de la condición, habrá o no iteraciones. Igual que antes, el loop
terminará cuando la condición del while tenga un resultado falso.

int i = 0;

do
{
Console.WriteLine($”Value: {i}”);
i++;
}
while (i < 15);
Try / Catch

Si bien ya hemos utilizado esta estructura de control y la explicamos


brevemente, sería interesante verla un poco más en detalle.
Cuando ejecutamos código en C# o en cualquier otro lenguaje de
programación pueden ocurrir distintos tipos de errores: errores cometidos por
el programador, errores cometidos por los valores que el usuario ingresa por
pantalla o errores inesperados.
Cuando un error ocurre en C#, en la gran mayoría de los casos la
secuencia que habíamos programado se detendrá y el sistema devolverá un
error; esto es lo que en realidad se conoce como una excepción.
Para poder anticiparnos a estos errores y capturarlos en tiempo de
ejecución, contamos con la estructura try catch.

try
{
//Block of code to try
}
catch (condition)
{
//Block of code to handle errors
}
finally
{
//Block of code that always run
}

La sentencia try permite definir un bloque de código que será testeado


por errores mientras es ejecutado. Cualquier error que ocurra dentro del
bloque de código del try será capturado por la instrucción catch, que a su vez
define un bloque de código para ser ejecutado justamente cuando el error es
capturado, y por último, la instrucción finally es opcional y sirve para definir
código que sí o sí se ejecutará al final de la secuencia, sin importar si un error
ha sido capturado o no.
Una vez finalizado el bloque de código del try o del catch (según
corresponda) el finally será ejecutado.

Veamos un ejemplo clásico del uso de un try catch.

string name = “Mark”;


int number = int.parse(name);

Aquí tenemos un string con una cadena de texto como cualquier otra y a
continuación una variable de tipo int y un intento por convertir ese string en
un int.
El ejemplo es bastante inocente porque evidentemente esto va a dar error:
no se puede convertir esa cadena de texto en un número entero.
La manera de capturar el posible error que el código devolverá es
envolverla con una estructura try catch:

try
{
string name = “Mark”;
int number = int.parse(name);
}
catch (Exception e)
{
Console.Writeline(“Cannot parse that”);
}

De este modo cuando el error ocurra, se ejecutará el bloque de código que


está dentro del catch y luego la aplicación seguirá normalmente y no romperá
las secuencias de código que le siguen. Si miramos detenidamente, notaremos
que hay una variable local de tipo Exception dentro del catch. Esta es una
clase que forma parte de .NET y que contiene los detalles específicos del
error que acaba de ocurrir; en el apartado práctico de esta lección veremos
qué cosas contiene esta clase y cómo podemos aprovecharla.

Hasta ahora hemos visto cómo manejar excepciones automáticamente


disparadas por .NET, pero también podemos disparar excepciones
manualmente utilizando la palabra clave throw.

string name = “John”;


if (name != “John”)
throw new Exception(“Name should be John”);
Excepciones

En C# todas las excepciones heredan directa o indirectamente de la clase


base System.Exception. Además, el lenguaje contiene un amplio repertorio
de excepciones disponibles según que tan específica sea la excepción que
necesitemos capturar.
Estas son algunas de las clases más importantes que derivan de
System.Exception:

System.IO.IOEException
System.OutOfRangeException
System.NullReferenceException
System.DivideByZeroException

IOException hace referencia a excepciones de tipo input output que se


utilizan exclusivamente para cuando estamos trabajando con archivos o
directorios.
Por ejemplo, si intentamos leer un archivo que no existe en disco,
entonces obtendremos una excepción de tipo IOException.
Una excepción de tipo IndexOutOfRange aparecerá cuando intentemos
acceder a una posición que no existe dentro en un array.
NullReferenceException aparecerá cuando intentemos utilizar el valor de
una variable nula y DivideByZeroException (como su nombre lo indica)
ocurrirá cuando el motor de .NET se encuentre con un intento de dividir por
cero.
Además de las excepciones ya disponibles en el lenguaje, siempre
podremos crear nuestras propias excepciones personalizadas simplemente
creando una clase y haciendo que herede de System.Exception, pero
estudiaremos herencia un poco más adelante.
Práctico

Entender cómo funciona el manejo de excepciones en C# es clave para


mantener nuestro código limpio y para mantenerlo libre de defectos.

Como siempre, empecemos con un pequeño ejemplo.

Console.WriteLine("Enter a number");
var input = int.Parse(Console.ReadLine());
Console.WriteLine($"The number is {input}, {input * 2}");

En el ejemplo imprimimos en consola una pequeña instrucción para el


usuario que ingrese un número, luego intentamos convertir el valor ingresado
por el usuario a int y almacenamos ese valor en una variable que llamaré
input.
Si el usuario ingresa un valor entero (como es esperado), entonces el
código se ejecutará normalmente y todo funcionará bien, pero si el usuario
ingresa cualquier cosa que no sea un valor entero la conversión no funcionará
y el sistema entregará un error.
Como regla general, no es una buena idea confiar en que el usuario
ingresará lo que se espera que ingrese y en cambio siempre hay que
prepararse para escenarios en donde el usuario hace lo inesperado. Es por eso
que aquí tenemos que agregar un try catch:

try
{
Console.WriteLine("Enter a number");
var input = int.Parse(Console.ReadLine());
Console.WriteLine($"The number is {input}, {input * 2}");
}
catch (Exception ex)
{
Console.WriteLine("Error: " + ex.Message);
}
finally
{
Console.WriteLine("Please retry...");
}

Si el usuario ingresa cualquier cosa que no sea un número entero,


veremos esto por pantalla:

Enter a number
asd
Error: Input string was not in a correct format.
Please retry...

La tercera línea del bloque de código que está en el cuerpo del try nunca
se ejecutará, ya que la segunda línea dispara el error y la secuencia del
programa salta directamente al cuerpo del catch. Luego de ejecutar el catch,
la ejecución se mueve hacia el bloque finally.

Si un bloque de código es lo suficientemente grande y complejo como


para que potencialmente exista más de un tipo de excepción, entonces puedo
agregar múltiples cláusulas catch con distintos tipos de datos de modo tal
que, según el tipo de excepción que ocurra, es el catch que se ejecutará. En
este sentido esto funciona como un switch: según el tipo de excepción, se
ejecutará el catch apropiado.
Algo importante a tener en cuenta es que las excepciones deben colocarse
de la más general a la más particular.
Colecciones
Array

Un array funciona como una colección de elementos, cualquiera sea su


tipo: cadenas de texto, números enteros, decimales, tipos de datos
personalizados, etc. El array suele utilizarse para agrupar elementos y luego
realizar diversas operaciones en ellos, como por ejemplo iterarlos, ordenarlos,
etcétera.
Los arrays se declaran como cualquier variable, con la diferencia de que
necesitamos agregar un par de corchetes después del tipo de datos:

string[] texts = new string[5];

Para poder usar un array, obligatoriamente necesitamos instanciarlo y


agregar la cantidad de elementos que vamos a agregar en él. En este ejemplo,
el número 5 especifica esa cantidad de elementos, esto significa que este
array en particular no podrá contener más de cinco elementos.
Para agregar un elemento al array, deberíamos escribir algo como esto:

texts[0] = “Quentin Tarantino”;

Como se puede ver, usamos el nombre del array seguido de los corchetes
y dentro de los corchetes escribimos el índice del elemento que vamos a
agregar, con esto indicamos en qué posición queremos agregar el elemento.
Recuerden que en C# el índice de todas las colecciones empiezan en cero, por
lo que el texto “Quentin Tarantino” estará en la primera ubicación del array.
Pero no es necesario seguir ningún orden a la hora de agregar elementos,
así que bien podríamos tener otra asignación como ésta, en un índice menor:

texts[3] = “Jack Lemon”;

También podemos declarar el array e inicializarlo directamente con todos


sus valores en la misma línea de la declaración, y cuando hacemos esto no
necesitamos especificar el tamaño del array.

Estas expresiones son equivalentes y se pueden usar indistintamente:


int[] numbers = int[4] {2, 7, 6, 9};
int[] numbers = {2, 7, 6, 9};

Los loops que vimos en la sección de estructuras de control, se utilizan


para recorrer colecciones. El for es la instrucción ideal para trabajar con
arrays:

for (int i=0; i < numbers.length; i++)


Console.WriteLine(numbers[i]);

Este tipo de expresiones es muy común ya que uno de los procesos más
comunes es el de iterar colecciones.
Algo interesante a notar en este ejemplo es que dentro del for tenemos
una variable entera i que comienza en cero y que luego, en la condición del
for, se determina la longitud del array utilizando la propiedad length de la
clase array. Lenght en español significa longitud.
Sabiendo esto, la condición se lee como sigue: “mientras i sea menor a
length, entonces el bloque de código se ejecutará”.
El bloque de código luego ejemplifica cómo acceder a los valores de los
elementos del array que es igual a la forma en la cual se asignan los
elementos del array: utilizando el nombre del array y colocando entre
corchetes el índice que queremos conocer en cada uno de los pasos de la
iteración.
En el ejemplo, el valor de un elemento del array se imprimirá por consola.
Práctico

Para declarar un array tenemos que definir el tipo de datos y acompañarlo


de dos corchetes; esa es la definición universal de un array. Luego podemos
dar un nombre, y podemos inicializarlo con la palabra clave new, utilizando
el tipo de datos y encerrando entre corchetes la cantidad de elementos que ese
array tendrá:

int[] numbers = new int[5];

Para asignar valores del array podemos utilizar sus índices:

numbers[0] = 2;
numbers[1] = 4;
numbers[4] = 8;

El índice es el número asociado a la posición que ese elemento tiene


dentro del array, empezando desde cero. Por lo tanto, si quisiera agregar el
primer elemento del array, tendré que escribir numbers y, entre corchetes,
cero, es decir, en su primera posición (su posición cero) le agrego un valor de
2. Lo mismo con el resto.

Para ver los valores del array también podemos utilizar sus índices:

Console.WriteLine(numbers[1]);

Esto imprimirá el valor del array en su posición 1:

Lo que importa saber aquí es que solamente podemos utilizar índices que
existen. Si yo quisiera acceder al valor del array en su índice 10, cuando
hemos declarado este array como una colección de 5 elementos, entonces
obtendré la excepción IndexOutOfRange que es otra forma de decir que el
índice al que estamos intentando acceder está fuera del rango definido por el
array que claramente va de cero a cuatro.
Esto es algo que hay que tener en cuenta cuando trabajamos con arrays,
porque es bastante fácil equivocarse dado que este tipo de colecciones suele
ser dinámica y no siempre sabemos que el elemento al que estamos
intentando ingresar es válido.

En general, la forma recomendada de acceder a las colecciones es a través


de loops:

int[] numbers = { 2, 4, 6, 8, 10, };

for (int i = 0; i < numbers.Length; i++)


{
Console.WriteLine(numbers[i]);
}

Como ya vimos, todos los arrays tienen una propiedad que devuelve la
cantidad de elementos que hay en la colección. Esta es la propiedad Length y
suele usarse en la condición de las estructuras del control para poder recorrer
todos los elementos de la colección.
De este modo, el for del ejemplo va a iterar hasta que i (que comienza en
cero y tiene un incremento de uno en cada una de las iteraciones), sea menor
a numbers.Length o la cantidad total de elementos del array.
En cada iteración se imprime el valor del array en esa posición utilizando
la variable i para acceder a la posición dinámica del array. Empezará en cero
y terminará en el último valor del array.

2
4
6
8
10

Así de fácil será también modificar valores dentro del array. Por ejemplo,
consideremos estos dos bucles:

int[] numbers = { 2, 4, 6, 8, 10, };

for (int i = 0; i < numbers.Length; i++)


{
numbers[i] = numbers[i] + 10;
}
for (int i = 0; i < numbers.Length; i++)
{
Console.WriteLine(numbers[i]);
}

Estas serán las salidas por consola:

12
14
16
18
20

En el primer loop modificamos los valores del array sumando 10


unidades a cada uno de los valores, y en el segundo los imprimimos.

Una estructura de control que vale la pena presentar ahora es el foreach,


ya que nos da la flexibilidad de recorrer toda la colección sin tener que hacer
uso de la propiedad Length:

int[] numbers = { 2, 4, 6, 8, 10, };


foreach (var item in numbers)
{
Console.WriteLine(item);
}

El foreach suele ser bastante más sencillo que el for en su sintaxis y nos
permite obtener el mismo resultado con menos código: simplemente recorrerá
todos los elementos que encuentre en la colección.
Listas

La lista es el tipo de colección más utilizado en C# por su versatilidad y


simplicidad, y por la potencia que ofrece su uso.

List<T>

Una lista de tipo T es una colección de objetos fuertemente tipados que


pueden ser accedidos por índice y que contiene métodos para ordenar
elementos dentro de la misma lista, para buscar uno o más elementos según
algún filtro, y para modificar elementos de la lista.

System.Collection.Generic;

La clase List pertenece al namespace Generic, que es un concepto clave


que explicaremos brevemente ahora, pero que es bastante más complejo
como para estudiar en un curso inicial porque confundiría algunas de las
características que hemos estudiado hasta aquí, así que lo dejaré fuera del
alcance de este libro aunque igual explicaremos lo básico.

A continuación de la palabra List, entre los símbolos de mayor y menor


aparece la palabra T. Básicamente, T es la representación genérica de
cualquier tipo de datos, o en otras palabras, podemos decir que T indica que
allí habrá un tipo de datos, pero no define exactamente cuál es el tipo de dato
particular. Este tipo de datos será especificado una vez que la instancia del
objeto sea creada.

List<int> numbers = new List<int>();

Este es el ejemplo de una instancia de la clase List. El tipo de datos es int


y es especificado entre los mismos símbolos mayor y menor a la hora de
instanciar el objeto.

Una vez definido el tipo de datos de la lista, sólo puede contener


elementos de ese tipo. .NET contiene un validador de tipos que verificará que
esta regla no se rompa.
En la sección del práctico veremos algunos de los métodos que nos ofrece
la clase List.
Práctico

Para agregar valores a una lista, podemos utilizar su método Add, que es
un método que heredan todos los tipos de colecciones genéricas. Hay que
recordar que sólo podemos agregar en la lista valores del tipo de datos que
definimos para la lista.

List<int> numbers = new List<int>();

numbers.Add(1);
numbers.Add(2);
numbers.Add(10);

var cities = new List<string>();

cities.Add("Lima");
cities.Add("Valencia");
cities.Add("Buenos Aires");

List<string> countries = new List<string>()


{
"Brasil",
"Bolivia",
"Colombia",
};

En el ejemplo hay tres listas distintas: una con valores de tipo entero y
dos con cadenas de texto, cada una inicializada de una manera diferente.
La primera es la que ya habíamos estudiado, pero la segunda demuestra
que podemos declarar e inicializar una lista de tipo implícito, en donde el
valor de la lista es inferido según el tipo de datos de sus valores, que están a
derecha del signo igual (=).
La última lista demuestra que otra forma de inicializar y agregar valores a
una lista es utilizando el inicializador de colecciones, en donde puedo escribir
los valores directamente entre llaves a la hora de instanciar la lista.
La lista también puede contener clases que pertenezcan a nuestro modelo
personalizado y no sólo contener tipos de datos primitivos:

using System;
using System.Collections.Generic;

namespace ConsoleApp8
{
class Program
{
static void Main(string[] args)
{
var customers = new List<Customer>();
customers.Add(new Customer() { ID = 1, Name = "John" });
customers.Add(new Customer() { ID = 2, Name = "Mark" });
}

public class Customer


{
public int ID { get; set; }
public string Name { get; set; }
}
}
}

El ejemplo ya se pone un poco más interesante, porque ahora puedo tener


mis propias clases, mis propios objetos que son instancias de esa clase, y
además puedo agruparlos a todos en listas. Y también podré operar con ellos.

¿Cómo podemos acceder a elementos de una lista? No hay diferencia


respecto a lo que veíamos en la sección de arrays ya que podemos acceder a
los elementos a través de su índice:

List<int> numbers = new List<int>() { 1, 4, 6, 8, 10 };

Console.WriteLine(numbers[4]);

Por consola obtendremos:


10

También podemos usar bucles para iterar en sus elementos, como en


cualquier otra colección:
List<int> numbers = new List<int>() { 1, 4, 6, 8, 10 };

foreach (var item in numbers)


{
Console.WriteLine(item);
}

Si no queremos usar foreach (que es lo más común y usado para recorrer


listas) , podemos usar la estructura for sin problemas, sólo que en lugar de
Length, tenemos que usar el método Count() que devolverá la cantidad de
elementos de la lista:

List<int> numbers = new List<int>() { 1, 4, 6, 8, 10 };

for (int i = 0; i < numbers.Count; i++)


{
Console.WriteLine(numbers[i]);
}

Para insertar elementos en nuestra lista, se puede usar el método Insert


que nos permite agregar elementos en la posición que necesitemos, y de la
misma forma, utilizando los índices, podemos eliminar elementos de la lista
utilizando el método Remove.
La clase List también ofrece el método Contains para determinar si un
elemento existe o no en determinada colección, entre muchos otros métodos
para operar con elementos dentro de la colección.
Diccionarios

En C# hay varios tipos de diccionarios, pero el más popular y el que más


se usa es el diccionario genérico que almacena valores key value (o clave
valor) sin ningún orden en particular.

Dictionary<TKey, TValue>

Dictionary<string, int> contacts = new Dictionary<string, int>();

Así es como se declara e inicializa un diccionario en C# y así es como


agregamos elementos al diccionario:

contacts.Add(“Vincent Vega”, 15);


contacts.Add(“Mia Wallace”, 63);

Como se puede ver, no es nada distinto a como se agregan elementos a


una lista genérica.
Hay que notar que en la declaración definimos un dictionary y, entre los
signos de mayor y menor, tenemos dos tipos de datos: string e int. En este
ejemplo, string va a representar el componente de la clave e int va a
representar el valor.
Y si vemos abajo en la asignación, podemos entender justamente como
funciona esto:
Primero insertamos la clave de tipo string (“Vincent Vega” en el primer
caso) y luego el valor entero (15), por lo tanto, si luego buscamos en esta lista
qué valor corresponde a la clave “Vincent Vega”, entonces obtendremos un
15.
Es decir que de alguna forma key es el elemento a través del cual luego
buscaremos en el diccionario,y value es el valor que queremos almacenar.
Entre las características de los Dictionary podemos destacar que los
diccionarios genéricos almacenan pares clave valor, forman parte del
namespace generic al igual que las listas, y sus claves deben ser únicas y no
pueden ser nulas.
Las claves deben ser únicas y no nulas porque, como dijimos, es a través
de las claves que vamos a poder recuperar luego los valores que estamos
almacenando. Los valores en cambio, sí pueden ser nulos o duplicados.
Práctico

Con lo que estudiamos hasta aquí, consideremos un diccionario con


algunos valores:

Dictionary<int, string> numberNames = new Dictionary<int, string>();

numberNames.Add(1, "One");
numberNames.Add(2, "Two");
numberNames.Add(3, "three");

Como toda colección, podemos acceder a los valores de este diccionario


con un foreach pero además contamos con un tipo de datos muy importante
que es KeyValuePair:

Dictionary<int, string> numberNames = new Dictionary<int, string>();

numberNames.Add(1, "One");
numberNames.Add(2, "Two");
numberNames.Add(3, "three");

foreach (KeyValuePair<int, string> item in numberNames)


{
Console.WriteLine($"Clave: {item.Key}, Valor: {item.Value}");
}

Por consola veremos esto:

Clave: 1, Valor: One


Clave: 2, Valor: Two
Clave: 3, Valor: three

Este tipo de datos nos ofrece las propiedades Key y Value para que
podamos obtener sin problema la clave y el valor del elemento de la
colección. Si no recuerdas el nombre del objeto tampoco importa mucho
porque recuerda que utilizando el tipo de dato implícito con var, el
compilador ya sabrá que el tipo de datos que necesitamos es KeyValuePair
porque será inferido a partir de la colección.

También podemos crear un diccionario con el inicializador de


colecciones:

var countries = new Dictionary<string, string>()


{
{"ARG", "Argentina" },
{"ESP", "España" },
{"PER", "Peru" },
{"MEX", "Mexico" }
};

Aquí he agregado elementos al diccionario sin usar el método Add,


directamente al momento de la inicialización del objeto.

Para recorrer e imprimir valores bastará con usar un foreach:

var countries = new Dictionary<string, string>()


{
{"ARG", "Argentina" },
{"ESP", "España" },
{"PER", "Peru" },
{"MEX", "Mexico" }
};

foreach (var item in countries)


Console.WriteLine($"Clave: {item.Key}, Valor: {item.Value}");

Claro que también podríamos acceder a elementos específicos del


diccionario, como en cualquier otra colección.

var countries = new Dictionary<string, string>()


{
{"ARG", "Argentina" },
{"ESP", "España" },
{"PER", "Peru" },
{"MEX", "Mexico" }
};

if (countries.ContainsKey("MEX"))
Console.WriteLine(countries["MEX"]);

Aquí aparece Contains.Key que es un método que se usa mucho a la hora


de trabajar con diccionarios por que permite saber si la clave está o no
presente en el diccionario. En el ejemplo, si la clave existe, entonces podré
escribir el valor de ese elemento en particular.
De manera similar, siempre a través de la clave, podemos actualizar los
valores del diccionario:

var countries = new Dictionary<string, string>()


{
{"ARG", "Argentina" },
{"ESP", "España" },
{"PER", "Peru" },
{"MEX", "Mexico" },
};

countries["MEX"] = "Colombia";

if (countries.ContainsKey("MEX"))
Console.WriteLine(countries["MEX"]);

Por consola veremos el nuevo valor modificado:

Colombia

Y eliminar valores del diccionario no sería diferente a lo que venimos


estudiante en este tipo de colecciones:

var countries = new Dictionary<string, string>()


{
{"ARG", "Argentina" },
{"ESP", "España" },
{"PER", "Peru" },
{"MEX", "Mexico" },
};

countries.Remove("PER");

foreach (var item in countries)


{
Console.WriteLine($"{item.Value}");
}

Por último, existe la posibilidad de limpiar todo el diccionario si así lo


quisiéramos:

countries.clear();

Esto directamente eliminará todos los valores del diccionario.


For Each

Algo ya vimos de esta estructura pero ahora que ya vimos colecciones,


podemos estudiarla por separado. For Each funciona de una manera similar
al for, sólo que opera directamente sobre colecciones de objetos:

ArrayList list = new ArrayList();


list.Add(“John Doe”);
list.Add(“Jane Doe”);
list.Add(“Someone Else”);

foreach(string name in list)


Console.WriteLine(name);

Un ArrayList funciona igual que un array, sólo que no necesitamos


especificar la cantidad de elementos que tendrá, ya que nos da la flexibilidad
de mantenerlo dinámico.

En el ejemplo vemos como el foreach itera la colección con una sintaxis


mínima.
Práctico

A diferencia del for,en el foreach no hay condiciones, ni incrementos, ni


variables a incrementar. Sólo hay una colección y esta estructura se asegura
de que para cada uno de los elementos de la lista se ejecute el bloque de
código que esté escrito en el cuerpo del foreach.

Veamos un ejemplo con un array de chars.

char[] anyWord = { 'c', 'o', 'd', 'e', 'r', ' ', 'c', 'a', 'v', 'e' };
foreach (var item in anyWord)
{
Console.WriteLine(item);
}

En la definición del foreach podemos ver como nos permite usar sin
problemas un tipo implícito para la variable local, lo cual nos da gran
versatilidad porque no tenemos que especificar exactamente cuál es el tipo de
datos cuando a veces, en porciones de código muy complejas, quizás ni
siquiera lo sabemos en el primer vistazo.

Por consola veremos esto:

c
o
d
e
r

c
a
v
e

Por cada uno de los elementos se ha ejecutado este bloque de código que
no hace otra cosa que imprimir en pantalla la cadena de texto.
Pero el foreach no sólo sirve para leer valores, sino que también podemos
modificar valores dentro de colecciones:

var numbers = new List<int> { 2, 6, 8, 12, 20, 40 };


int sum = 0;
foreach (int number in numbers)
{
sum += number;
}
Console.WriteLine(sum);

Al ejecutar este código, esto aparecerá en pantalla:

88

Lo que esperamos que suceda aquí es que por cada uno de estos números
la suma crezca y vaya acumulando los valores, y eso es exactamente lo que
sucede.

Así de sencillo es usar el foreach, pero una de sus características más


importantes es que nos permite iterar de manera muy sencilla, colecciones de
tipos de datos no primitivos:

using System;
using System.Collections.Generic;

namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
List<Customer> customers = new List<Customer>();
customers.Add(new Customer { Name = "John Wick" });
customers.Add(new Customer { Name = "John Travolta" });
customers.Add(new Customer { Name = "Tony Soprano" });

foreach (Customer cus in customers)


{
Console.WriteLine(cus.Name);
}
}
public class Customer
{
public string Name { get; set; }
public int Id { get; set; }
}
}
}

Y aquí es en donde, sin dudas, aparece la verdadera potencia del foreach:


en la posibilidad de operar con nuestros propios tipos de datos de una manera
simple, clara y limpia.
Programación orientada a objetos
Encapsulamiento

El encapsulamiento es el primer pilar de la programación orientada a


objetos .
De hecho, algo de esto ya vimos sin todavía nombrarlo cuando veíamos
propiedades en clases. Encapsulamiento es el proceso de encapsular campos
y métodos dentro de una clase y surge de la necesidad de evitar que
determinados datos sean corrompidos debido a errores accidentales a la hora
de escribir código.

class Person
{
private string name;

public string name


{
get { return name; }
set { name = value; }
}
}

Las propiedades pueden pensarse como la herramienta que C# ofrece para


poner en práctica el encapsulamiento.

Analicemos la clase Person. Aquí tenemos un campo name de tipo string


dentro de la clase Person. Este campo es de tipo privado, lo que significa que
no es accesible por fuera de la clase, de esta forma se protege el valor del
campo name, haciendo imposible que su valor cambie por fuera de la clase y
dejando el control absoluto de su valor dentro de la propia clase Person.
A continuación del campo tenemos la propiedad name, que es pública y
que ofrece el acceso centralizado al valor de esa variable.
Allí se puede actualizar el valor a través del set o devolver el valor a
través del get; no existe ni dentro de la clase Person ni fuera de la clase
Person, otro lugar en donde el valor de name pueda ser actualizado.
Este es un ejemplo clásico de encapsulamiento. Pero una propiedad puede
incluir también algo de comportamiento:
private string name;
public string name
{
get {return “The name is” + name;}
set {
if (value.Contains(“,”);
throw new Exception(“Error”);
name = value;
}
}

Con este ejemplo ampliamos el concepto y vemos como el campo name


puede sufrir alteraciones dentro de la propiedad, pero una vez más, este es el
único lugar en donde eso sucede, por lo que podemos estar seguros de que
cada vez que usemos el get de la propiedad, el valor no habrá sido
accidentalmente reemplazado por otro.
En otras palabras, el encapsulamiento básicamente consiste en brindar un
acceso centralizado a la actualización del campo y que ese campo no pueda
ser actualizado desde ningún otro lugar que no sea su propia propiedad.
Herencia

Otro de los pilares de la programación orientada a objetos es la herencia,


que además es uno de los conceptos sobre los cuales C# se sostiene.
Herencia es la habilidad de crear clases que heredan ciertos aspectos de
sus clases padres. La plataforma .NET se ha construido en base a este
concepto porque todos y cada uno de los elementos del lenguaje heredan de
la clase Object, es por eso que se dice que en .NET todo es un objeto.

Para entender la herencia veamos un diagrama sencillo:


Supongamos que una clase A tiene una cierta propiedad y un cierto
método: propiedadA y métodoA. Herencia significa que podemos tener una
clase B que herede de la clase A y que, por lo tanto, herede también sus dos
miembros.
Es decir que propiedadA y métodoA pasan a ser también miembros de la
clase B y pueden ser utilizados de la misma manera en que serían utilizados
en la clase A.

Pero no sólo eso, sino que además la clase B puede también contener sus
propios métodos, en este caso, una propiedad y un método.

El ejemplo clásico de herencia sería el siguiente:

public class Person


{
public string FirstName {get; set;}
public string LastName {get; set;}
};

public class Employee : Person


{
public int ID {get; set;}
public string Office {get; set;}
};

Una clase persona que contiene dos propiedades FirstName y LastName,


dos características básicas de cualquier persona.
Luego una clase empleado que hereda de la clase persona: es decir que un
empleado es una persona. Esta es la mejor forma de pensar en herencia entre
clases.
Entonces, como un empleado es una persona, contiene FirstName y
LastName gracias a heredarlos de Person, pero además agrega dos
propiedades exclusivas de un empleado como son ID y Office.
La clase empleado es la representación de una persona en relación a su
puesto de trabajo, mientras que la clase Person es la representación de una
persona en general, pero en definitiva un empleado es una persona por lo que
tiene sentido que herede esas características.
Un objeto Person (instancia de la clase Person) sólo tiene acceso a las
propiedades FirstName y LastName, mientras que una instancia de la clase
empleado tendrá acceso a las propiedades FirstName y LastName, más las
dos propiedades exclusivas de empleado.
La herencia se aplica en C# simplemente agregando a continuación de la
clase hija dos puntos y el nombre de la clase a heredar, como se ve en el
ejemplo.
Práctico

La mejor forma de entender la herencia es con un ejemplo bien sencillo y


práctico:

using System;

namespace ConsoleApp10
{
class Program
{
static void Main(string[] args)
{
Animal animal = new Animal();
animal.Saludar();
Perro perro = new Perro();
perro.Saludar();
}
}

public class Animal


{
public void Saludar()
{
Console.WriteLine("Hello I am an animal");
}
}

public class Perro : Animal


{

}
}

En el ejemplo tenemos una clase pública llamada animal. Esta clase


animal tendrá un método de tipo void llamado Saludar, y dentro del método
Saludar voy a agregar una impresión que dirá “Hello I am an animal”.
Además de la clase animal, voy a agregar una clase para un animal más
específico: una clase perro.
Lo primero que puedo decir de esta clase es que es un animal, por lo
tanto, lo que sea que yo tenga escrito en animal, cualesquiera sean sus
propiedades, sus campos o sus métodos, deben aplicar también para mi clase
perro, por lo tanto, con los dos puntos voy a decirle a .Net que mi clase Perro
hereda de animal. En otras palabras, que Perro es un animal.

Si dejamos el código así y ejecutamos, obtenemos esto:

Hello I am an animal
Hello I am an animal

A pesar de no haber definido Saludar en mi clase perro, de cualquier


forma, la clase todavía sabe cómo tiene que saludar porque lo heredó de la
clase animal.
Sin embargo esto suena un poquito genérico si tengo un animal que
saluda como un animal, pero tengo un perro que debería ser capaz de saludar
como un perro, asi que vamos a adaptarlo un poco a nuestras necesidades:
cuando el perro o cualquiera sea mi otra clase no sepa cómo saludar, entonces
puede decir genéricamente “Hello I am an animal”, pero si en cambio sí sabe
cómo saludar, entonces que este comportamiento sea reemplazado por el
saludo propio de ese animal particular:

using System;

namespace ConsoleApp10
{
class Program
{
static void Main(string[] args)
{
Animal animal = new Animal();
animal.Saludar();
Perro perro = new Perro();
perro.Saludar();
Gato gato = new Gato();
gato.Saludar();
}
}

public class Animal


{
public virtual void Saludar()
{
Console.WriteLine("Hello I am an animal");
}
}

public class Perro : Animal


{
public override void Saludar()
{
Console.WriteLine("Hello I am a dog");
}
}

public class Gato : Animal


{

}
}

Lo que hago aquí es agregar la palabra clave virtual en la firma del


método de la clase a heredar, y en la clase heredada agrego la palabra
override.
Básicamente lo que estoy haciendo es definiendo un comportamiento
genérico, y declarando que este método puede ser sobrescrito, y luego con
override estoy justamente sobreescribiendo la implementación del método.
Esto es interesante porque si yo tengo otra clase aquí, por ejemplo Gato,
que también implementa de animal, sí que podría tener esa clase vacía y
cómo este animal no sabe saludar, entonces va a usar la implementación
virtual de la clase animal. En cambio, como mi perro sí sabe saludar,
entonces sobreescribirá la implementación del método saludar de la clase
animal.
Ejecutar el ejemplo de arriba imprimirá esto:

Hello I am an animal
Hello I am a dog
Hello I am an animal

Ahora incluyamos voy a incluir también un saludo a mi clase de gato para


que deje de usar el saludo genérico y utilice uno propio:

using System;

namespace ConsoleApp10
{
class Program
{
static void Main(string[] args)
{
Animal animal = new Animal();
animal.Saludar();
Perro perro = new Perro();
perro.Saludar();
Gato gato = new Gato();
gato.Saludar();
}
}
public class Animal
{
public virtual void Saludar()
{
Console.WriteLine("Hello I am an animal");
}
}
public class Perro : Animal
{
public override void Saludar()
{
Console.WriteLine("Hello I am a dog");
}
}
public class Gato : Animal
{
public override void Saludar()
{
Console.WriteLine("Hello I am a cat");
}
}
}

Y el resultado será:

Hello I am an animal
Hello I am a dog
Hello I am a cat

En C# no se puede sobreescribir los miembros de la clase a menos que


hayan sido marcados como virtuales con la palabra clave virtual.
Clases abstractas

Las clases abstractas se definen con la palabra clave abstract y son


utilizadas para indicar cuál es la clase base de la jerarquía.
Lo que hace especiales a las clases abstractas es que no pueden ser
instanciadas, de hecho el compilador validará en tiempo de compilación que
no intentes crear una instancia de una clase abstracta. Todo lo que puedes
hacer es crear una subclase o una clase que herede de la clase abstracta.
Volvamos al ejemplo de Person y Employee.

public abstract class Person


{
public string FirstName {get; set;}
public string LastName {get; set;}
};
public class Employee : Person
{
public int ID {get; set;}
public string Office {get; set;}
};

Pero ahora pensemos en la clase Person como una clase abstracta.


En cuanto a la sintaxis, el único cambio es el de la palabra clave abstract
justo antes de class en la clase Person, pero en la práctica ya no podemos
instanciar Person como antes.

Si ahora queremos tener dos objetos separados, derivados de cada una de


las clases, el compilador no nos permitirá crear una instancia de la clase
Person y la única forma de acceder a sus propiedades FirstName y LastName
será a través de clases que hereden de Person.
Esto sirve, entre otras cosas, para definir el comportamiento de un grupo
de clases, porque cuando creamos una clase abstracta como esta, estamos
obligando a cualquier otra clase que quiera usar estas propiedades a heredar e
implementar los miembros de Person. Esto puede no tener mucho sentido si
pensamos sólo en propiedades, pero qué pasa si definimos un método en una
clase abstracta:
public abstract class Person
{
public string FirstName {get; set;}
public string LastName {get; set;}
public abstract bool IsActive();
};

En clases abstractas podemos definir métodos abstractos también


definidos por la palabra clave abstract.
En este ejemplo he agregado el método IsActive a la clase Person y, como
se puede ver, este método no tiene cuerpo; es sólo la firma del método sin
ninguna implementación.
Y este método no tiene implementación porque en la clase Person no es
importante cómo será implementado este método, sino que todas las clases lo
implementen.
A decir verdad, un método abstracto en una clase abstracta es
básicamente una obligación para que todas las clases que hereden de esa clase
implementen el método, y aquí es en donde puede apreciarse mejor el
beneficio de las clases abstractas, ya que ofrecen una manera de abstraer el
comportamiento de una clase, obligando a que las clases derivadas cumplan
con ciertos requisitos, pero sin especificar exactamente cómo estos requisitos
deben ser cumplidos.
Esto nos da pie para hablar del tercer pilar de la programación orientada a
objetos: el polimorfismo.
Práctico
Para entender todo esto de clases abstractas volvamos al ejemplo con el
cual aprendimos y practicamos herencia. Aquí tenemos una clase animal y
además dos clases: perro y gato. En el ejemplo tenemos una instancia de la
clase animal, por lo cual si agregamos la palabra clave abstract,
inmediatamente obtendremos un error del compilador que dice “cannot
create an instance of the abstract class or interface animal”: no se puede
crear una instancia de la clase abstracta animal. Entonces este objeto ya no
puede existir más.

using System;

namespace ConsoleApp10
{
class Program
{
static void Main(string[] args)
{
Animal animal = new Animal();
animal.Saludar();
Perro perro = new Perro();
perro.Saludar();
Gato gato = new Gato();
gato.Saludar();
}
}
public abstract class Animal
{
public virtual void Saludar()
{
Console.WriteLine("Hello I am an animal");
}
}
public class Perro : Animal
{
public override void Saludar()
{
Console.WriteLine("Hello I am a dog");
}
}
public class Gato : Animal
{
public override void Saludar()
{
Console.WriteLine("Hello I am a cat");
}
}
}

Si pensamos un segundo esto tiene sentido, porque en general vamos a


necesitar lo específico del perro y el gato: que el perro pueda decir “soy un
perro”, que el gato pueda decir “soy un gato” y que la clase animal solamente
sirva para agrupar propiedades comunes a todos los animales.
El de las clases abstractas es un concepto muy importante en
programación, principalmente porque implica pensar a futuro: puede que hoy
nuestro sistema soporte solamente perros y gatos, pero la definición de la
clase animal y escribir lógica alrededor de esta clase animal nos permite que
en el futuro, si nuestro sistema se expande, podamos agregar con gran
facilidad otros animales teniendo básicamente lo principal ya definido y
fundamentado.
Polimorfismo

La mejor forma de entender el polimorfismo es con un ejemplo bien


gráfico:

Supongamos que tenemos una clase abstracta Shape que tiene dos
propiedades: Height y Width y además un método CalculateArea().
A la clase es abstracta no le importa cómo este método es implementado
porque nunca habrá una instancia de Shape, pero sí importa que cualquier
clase que herede de Shape implemente de alguna manera el método
CalculateArea.

Imaginemos ahora tres clases derivadas de Shape: cuadrado, rectángulo y


triángulo. Obviamente las tres formas son diferentes y la ecuación para
calcular el área de cada una de ellas es diferente, pero la esencia es la misma:
las tres tienen alto y ancho y con esas propiedades se puede conocer el área
de cada una de ellas.
Esto es el polimorfismo porque se dice que la clase abstracta Shape y sus
miembros pueden tomar muchas formas.
Práctico

Veamos el ejemplo anterior traducido a código C#:

public abstract class Shape


{
public int Height { get; set; }

public int Width { get; set; }


public abstract double CalculateArea();
}

public class Square : Shape


{
public override double CalculateArea()
{
return Math.Pow(Width, 2);
}
}

public class Rectangle : Shape


{
public override double CalculateArea()
{
return Width * Height;
}
}

public class Triangle : Shape


{
public override double CalculateArea()
{
return (Width * Height) / 2;
}
}
Ahí tenemos básicamente cómo calcular el área para cada una de las
formas. Esto es el polimorfismo: se trata de definir un método o un
comportamiento general.
En este ejemplo todas las formas o las figuras geométricas tienen la
capacidad de calcular su propia área, aunque cada cálculo será distinto según
cuál sea esa figura geométrica. Mientras la figura tenga las dos propiedades y
una implementación del método, entonces lo único que cambia es la forma en
la que calculamos el área.
Ahora creemos algunas figuras y le pidamos que cada una calcule su
propia área:

using System;
using System.Collections.Generic;

namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
var shapes = new List<Shape>();
var rectangle = new Rectangle() { Width = 20, Height = 10 };
var triangle = new Triangle() { Width = 10, Height = 15 };
var square = new Square() { Width = 10 };

shapes.Add(rectangle);
shapes.Add(triangle);
shapes.Add(square);

foreach (var item in shapes)


{
Console.WriteLine(item.CalculateArea());
}

}
}

public abstract class Shape


{
public int Height { get; set; }

public int Width { get; set; }


public abstract double CalculateArea();

}
public class Square : Shape
{
public override double CalculateArea()
{
return Math.Pow(Width, 2);
}
}
public class Rectangle : Shape
{
public override double CalculateArea()
{
return Width * Height;
}
}
public class Triangle : Shape
{
public override double CalculateArea()
{
return (Width * Height) / 2;
}
}
}

Es importante notar que estoy instanciando sólo los derivados de la clase


Shape. Luego vamos a agregar a nuestra lista de Shapes: un rectángulo, un
cuadrado y un triángulo, y con un foreach iteramos la colección para que
podamos imprimir cada una de los cálculos.

Esto veremos por consola al ejecutar:

200
75
100
Interfaces

Algunas lecciones atrás aprendimos qué cosa eran las clases abstractas.
Lo interesante de esto es que las interfaces son en realidad bastante parecidas
a las clases abstractas, especialmente porque tampoco pueden ser
instanciadas.
Sin embargo, las interfaces son todavía un poco más conceptuales que las
clases abstractas, porque sólo aceptan la firma de los métodos pero no sus
implementaciones. Como no hay implementación de métodos, tampoco habrá
propiedades ni tendría mucho sentido agregar campos, por lo que se podría
decir que una interfaz es esencialmente una clase abstracta que sólo tiene
métodos abstractos.

En general, se dice que una interfaz es un contrato porque toda clase que
implemente la interfaz está obligada a implementar todos sus métodos.

Pero lo más importante a aprender respecto a Interfaces, es que C#


permite que una clase implemente múltiples interfaces, y esta es una forma de
contrarrestar el hecho de que C# no permite la herencia múltiple, es decir que
una clase no puede heredar de más de una clase pero sí que puede
implementar múltiples contratos o interfaces.

Supongamos dos clases diferentes: tren y auto.


Si bien estas dos clases son distintas y tendrán diferentes propiedades y
métodos, las dos comparten un mismo comportamiento: ambas clases
permiten desplazarse. Como ambas realizan el movimiento de maneras
diferentes, es lógico pensar que sus implementaciones también sean
diferentes.
La importancia de las interfaces radica en las herramientas que brindan al
programador a la hora de construir una arquitectura sostenible y mantenible,
ya que si ahora en mi sistema necesito dar soporte a, por ejemplo, aviones,
con solo hacer que mi clase implemente de la interfaz ya lo consigo.

Entonces las interfaces, al igual que las clases abstractas, no pueden ser
instanciadas, sus métodos no tienen cuerpo, y el cuerpo o la implementación
del método estarán siempre en la clase que implementa la interfaz. Cuando
una interfaz es implementada, la clase que la implementa debe sobreescribir y
agregar cuerpo a todos sus métodos, sin excepción.
Las interfaces pueden contener propiedades y métodos, pero no campos o
variables.
Además, por defecto, todos sus métodos son public y abstract, aun
cuando no lo especificamos explícitamente.
Práctico

Para ver este ejemplo de interfaces vamos a hacer algo muy, muy
parecido a lo que hicimos con herencia y con clases abstractas:

public interface IVehicle


{
void SpeedUp(int increment);
void applyBreaks(int decrement);
}

Este es el contrato de una interfaz de vehículos. Con esta interfaz estoy


definiendo que todos los vehículos de mi sistema deben implementar, al
menos, estos dos métodos. No defino cómo deben ser implementados pero sí
que las implementaciones deben existir siempre que la clase implemente la
interfaz.

public class Bicycle : IVehicle


{
int speed;
public void applyBreaks(int decrement)
{
speed = speed - decrement;
}

public void SpeedUp(int increment)


{
speed = speed + increment;
}
}

Esta es una clase bicicleta que implementa la interfaz e implementa los


métodos que el contrato obliga, a su manera.
La clase tiene un valor speed (velocidad) y en los métodos de acelerar o
frenar aplica un incremento o un decremento a la velocidad según
corresponda.
Ahora bien, además de los métodos que tiene la interfaz, también puedo
agregar otro método propio de esta clase que no tenga nada que ver con la
interfaz:

public class Bicycle : IVehicle


{
int speed;
public void applyBreaks(int decrement)
{
speed = speed - decrement;
}

public void SpeedUp(int increment)


{
speed = speed + increment;
}
public void Print()
{
Console.WriteLine($"Speed. {speed}");
}
}

Ahora el nuevo método imprimirá en pantalla la velocidad actual de la


bicicleta.

Luego agregaré una nueva clase que implemente la interfaz, pero de


forma levemente distinta:

class Car : IVehicle


{
int speed;
public void applyBreaks(int decrement)
{
speed = speed - decrement * 5;
}
public void SpeedUp(int increment)
{
speed = speed + increment * 5;
}
public void Print()
{
Console.WriteLine($"Speed: {speed}");
}
}

Las implementaciones son parecidas, pero en la clase auto los valores de


velocidad se multiplican por 5 sin importar cual sea el número que reciban
por parámetros. Tenemos entonces dos clases que implementan de manera
diferente los dos métodos de nuestra interfaz. Ambas son vehículos, ambas
cumplen con tener una lógica para acelerar y una lógica para frenar aunque
las lógicas son diferentes.

Ahora vamos a terminar con el ejemplo definiendo un valor para


incremento y decremento y ejecutando los métodos de ambas clases:

class Program
{
static void Main(string[] args)
{
int increment = 10;
int decrement = 5;
Bicycle bicycle = new Bicycle();
bicycle.applyBreaks(decrement);
bicycle.SpeedUp(increment);

Car car = new Car();


car.applyBreaks(decrement);
car.SpeedUp(increment);

bicycle.Print();

car.Print();
}
}

Analicemos el resultado:
Speed. 5
Speed: 25

Con los mismos valores de entrada, los dos vehículos resolvieron su


velocidad de manera diferente, aunque ahora la arquitectura permite agregar
múltiples vehículos y que cada uno defina su propia manera de frenar y
acelerar.
Conceptos Avanzados
Tipos Anónimos

Hasta ahora hemos aprendido que los objetos son instancias de una clase.
Una clase se declara con un cierto número de campos, propiedades y
métodos, y luego podremos crear una instancia de esa clase en la forma de
objeto. Sin embargo, con la introducción de tipos anónimos, ahora también
podemos crear un objeto sin tener la necesidad de declarar una clase.
Si bien esto suena a que las clases ya no son necesarias, en la práctica, no
hay nada más lejos de la verdad. Los tipos anónimos tienen muchas
limitaciones y están pensados para ser usados en situaciones en donde
necesitamos crear un objeto rápido y con pocos elementos.

var customer new


{
Name = “Tony Soprano”
Age = 51
};

Console.WriteLine(customer.Name + “: ” + customer.Age);

Un tipo anónimo se inicializa usando el operador new seguido del objeto


a inicializar.
En ese sentido es igual a instanciar una clase sólo que sin agregar el
nombre de la clase. Además, como no hay ninguna clase que soporte el nuevo
objeto, es necesario utilizar la palabra clave var de las variables de tipos
implícitos.
Más allá de esto, el objeto anónimo se utiliza de la misma forma en la que
se utilizaría cualquier otro objeto.

Además, un tipo anónimo puede también incluir otros tipos anónimos:

var customer new


{
name = “Tony Soprano”
Age = 51
Address = new { Street = “St Louis”, No = 16}
};

En este ejemplo, dentro de la variable anónima customer tenemos la


propiedad Address, que a su vez representa otro objeto anónimo.

Si fuera necesario, también podríamos crear un array de tipos anónimos:

var contacts new[]


{
new { FirstName = “Tony”, LastName = “Soprano” },
new { FirstName = “Marlon”, LastName = “Brando” },
new { FirstName = “Orson”, LastName = “Welles” }
};

Aquí hay un array que se llama contacts y dentro de ese array tenemos
tres objetos de tipo anónimo.

Los tipos anónimos son útiles para muchas situaciones, pero en especial
para cuando se necesita devolver un conjunto de valores con una complejidad
mayor que la de simplemente un string o un número, rápidamente.
Los tipos anónimos también permiten crear propiedades dinámicamente
sin la necesidad de declarar una clase primero.

Pero los tipos anónimos también tienen grandes limitaciones:


● Sólo pueden contener propiedades, es decir que, a diferencia de una
clase real, no pueden contener ni campos ni métodos, sólo
propiedades.
● Una vez que el objeto sea inicializado, no puedes agregarle
propiedades, es decir, no puedes modificar ese objeto.
● Una vez que lo has declarado, las propiedades son de sólo lectura,
esto significa que una vez inicializado el tipo anónimo no puedes ni
siquiera cambiar sus valores. En otras palabras, la variable anónima
es inmutable.
Tipos Dinámicos

En todos los conceptos que hemos aprendido hasta aquí en este libro
siempre hemos dicho que el compilador de C#, en tiempo de compilación,
puede detectar determinados errores cuando trabajamos con tipos de datos
además de validar sintaxis, entre muchas otras cosas.
Los tipos dinámicos básicamente evitan que el compilador valide el tipo
de dato de las variables dinámicas en tiempo de compilación. En su lugar, la
validación ocurrirá sólo en tiempo de ejecución.
Esto significa que no importa qué cosa hagamos con la variable de tipo
dinámico, la aplicación siempre compilará, aunque más tarde en tiempo de
ejecución la aplicación arrojará un error si hay alguna inconsistencia.

dynamic x = 100;

Una variable dinámica se define usando la palabra clave dynamic. En


tiempo de compilación, el compilador tratará a esta variable como si fuera de
tipo object, que es la clase de la cual heredan todas las clases en C#. Esta es
la forma más genérica que puede tomar una variable en tiempo de
compilación, sin embargo, el verdadero tipo de la variable será resuelto en
tiempo de ejecución.
Una forma de comprobarlo es usando el método GetType de la clase
Object:

Console.WriteLine(x.GetType());

El resultado sería:

System.int32

Lo interesante de los tipos dinámicos es que al no tener el control de tipos


del compilador, la variable puede ser convertida a otros tipos implícitamente:

dynamic x = 100;

x = “Hi there”;
x = true;

x = DateTime.Today;

Los tipos dinámicos ofrecen en C# algunas de las ventajas de los


lenguajes débilmente tipados, mientras que las ventajas de los lenguajes
fuertemente tipados rigen para el resto de elementos del lenguaje. También
podemos usar el tipado dinámico para asignar instancias de una clase, aunque
es aquí en donde notaremos los riesgos de usar tipos dinámicos.

Veamos un ejemplo:

public class contact


{
public void GetPhone(int id)
{

}
}

Supongamos que tenemos una clase contact con un único método


GetPhone que recibe un id de tipo int.
Ahora vamos a crear una variable dinámica, que será una instancia de la
clase contacto.

dynamic con = new contact();

con.GetPhone();

Una vez tenemos esta variable dinámica, tengamos en cuenta que el


compilador no va a verificar que utilicemos los métodos y las propiedades
correctas cómo sería con un tipo de datos normal. Y es aquí donde aparece el
riesgo, porque nada impide que yo escriba intente utilizar el método
GetPhone sin pasarle ningún parámetro como hice en el ejemplo. En este
caso la aplicación compilará sin problemas, pero luego en tiempo de
ejecución devolverá un error.
Lo mismo sucedería si en lugar de pasar un int como argumento, enviara
una cadena de texto: no hay aviso posible por parte del compilador de que
estoy a punto de cometer un error, en cambio, el error se producirá cuando la
aplicación se esté ejecutando.
Tampoco habrá ningún tipo de advertencia si intento usar un método que
no existe. El compilador no validará absolutamente nada respecto de la
variable dinámica y el error aparecerá cuando quizás sea demasiado tarde.
Es por eso que hay que tener mucho cuidado a la hora de utilizar
variables dinámicas, especialmente cuando estamos acostumbrados a trabajar
con lenguajes fuertemente tipados, ya que no recibiremos la ayuda del
compilador, sino que dependerá enteramente de nosotros que el código que
estemos escribiendo sea válido o no.
Tipos Nullable

Null o nulo literalmente significa nada; representa el valor de una


variable que todavía no tiene ningún valor. Como regla general, en C# es
importante siempre asegurarse de que la variable tiene valor antes de intentar
acceder a su valor de otra manera hay grandes chances de obtener la conocida
NullReferenceException que es probablemente la más popular de las
excepciones entre programadores .NET.
Los valores nulos son especialmente importantes cuando trabajas con tus
propios objetos y con strings. En muchos casos sucederá que necesitas
indicar que tu variable será nula porque planeas que el valor se le asigne más
tarde.

Car car = null;

Esto se puede hacer sencillamente con la asignación y más tarde puedes


verificar si el valor de la variable efectivamente es nulo simplemente
comparándolo con la palabra clave null.

if(car == null)

Por otro lado, cuando trabajamos con números, notaremos que los
números siempre tienen un valor por defecto. Para un int o un long este valor
por defecto es cero (0), que no debe ser confundido con null porque no es lo
mismo. Null representa la ausencia de valor, mientras que 0 es un valor tan
válido como cualquier otro. Entonces, si alguna vez necesitas tener un entero
con un valor nulo, no podrás hacerlo de manera directa por qué no se puede
asignar null a una variable de tipo int.

int number = null;

La sentencia de arriba entregará un error, a menos que ese tipo de datos


int haya sido definido como Nullable, es decir, como una variable que acepta
valores nulos.
Esto lo hacemos así:
int? numberNullable = null;

Una variable se define Nullable al agregar un signo de pregunta justo


después del tipo de datos.
Una vez definida la variable que acepta valores nulos, necesitaremos
poder comprobar si el valor de la variable es efectivamente nulo o no. Hay
dos formas de realizar esta verificación; la primera y la más fácil es
simplemente comparar la variable con la palabra clave null como harías con
cualquier otro tipo de datos:

if(numberNullable == null)

La segunda manera de hacerlo es utilizando la propiedad HasValue que


todos los objetos Nullables heredan automáticamente:

if(! numberNullable.HasValue)

No hay ninguna diferencia entre una manera y la otra, se puede usar


cualquiera de las dos indistintamente.

Por último, para devolver el valor de la variable (si lo hubiere), los tipos
de datos Nullables también heredan la propiedad Value que se usa justamente
para esto:

Console.WriteLine(numberNullable.Value);
Práctico

Como acabamos de ver, si quisiéramos asignar un valor nulo a una


variable int no podríamos, inmediatamente obtendremos un error del
compilador diciendo que no se puede porque este no es un tipo Nullable.
Sin embargo en muchos casos será útil permitir que esta variable puede
almacenar un valor nulo y la forma técnicamente correcta de hacerlo sería
esta:

Nullable<int> x = null;

Al escribir esto no habrá errores, pero si intentamos acceder al valor de x


así sí obtendremos un error ya que la variable no tiene realmente ningún
valor. La excepción que arrojará la aplicación será
InvalidOperationException, con el mensaje de que una variable nula debe
tener un valor.
Una de las opciones que tenemos en este caso es la de utilizar el método
GetValueOrDefault, el cual devolverá el valor por defecto del tipo int cuando
la variable sea null.

Nullable<int> x = null;

Console.WriteLine(x.GetValueOrDefault());

El resultado de ejecutar esto será el siguiente:

Aún cuando nuestra variable no tiene valor, obtenemos su valor por


defecto según su tipo de datos.

Si bien la palabra Nullable es la manera más clara y legible de declarar


una variable de este tipo, rara vez la verás declarada de este modo. En cambio
verás lo que hemos visto en el teórico: el signo de preguntas junto al tipo de
datos.
int? x = null;

Console.WriteLine(x.GetValueOrDefault());

Además de permitir valores nulos, una variable Nullable se comporta


como cualquier otra y puede ser asignada con cualquier valor del tipo de
datos definido, por ejemplo:

int? x = null;
x = 10;
Console.WriteLine(x.GetValueOrDefault());

Como es de esperar, al ejecutar obtendré el valor que le asigné a la


variable:

10

Lo más importante de entender respecto de las variables Nullable es:


cómo declararlas, cómo determinar si tienen o no valor y cómo recuperar el
valor de la variable o, si es nulo, su valor por defecto.

También podría gustarte