Skip to content

Instantly share code, notes, and snippets.

@stifskere
Last active June 4, 2024 20:06
Show Gist options
  • Save stifskere/49c265f957ded3877aec2cef4a59d28c to your computer and use it in GitHub Desktop.
Save stifskere/49c265f957ded3877aec2cef4a59d28c to your computer and use it in GitHub Desktop.
Un tutorial de C# en español.
/*
C# Tutorial © 2024 by Esteve Autet Alexe is licensed under
Creative Commons Attribution-ShareAlike 4.0 International.
To view a copy of this license, visit https://creativecommons.org/licenses/by-sa/4.0/
*/
// LOS SIGUIENTES WARNING ESTAN DESHABILITADOS POR QUE ESTO ES UNA GUIA.
using System.Collections;
using System.Runtime.InteropServices;
using System.Text;
#pragma warning disable CS0219
#pragma warning disable CS0642
#pragma warning disable CS0162
#pragma warning disable CS8600
#pragma warning disable CS0168
#pragma warning disable CS0414
#pragma warning disable CS0067
#pragma warning disable CA1821
#pragma warning disable CA1050
// ReSharper disable UnusedNullableDirective
// ReSharper disable EmptyRegion
// ReSharper disable EmptyDestructor
// ReSharper disable HeuristicUnreachableCode
// ReSharper disable ConditionIsAlwaysTrueOrFalse
// ReSharper disable ConvertToConstant.Local
// ReSharper disable UnusedVariable
// ReSharper disable RedundantAssignment
// ReSharper disable once MergeIntoPattern
// ReSharper disable ForCanBeConvertedToForeach
// ReSharper disable RedundantEmptySwitchSection
// ReSharper disable RedundantJumpStatement
// ReSharper disable LoopCanBeConvertedToQuery
// ReSharper disable RedundantEmptyFinallyBlock
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable UnusedMember.Global
// ReSharper disable ReplaceAutoPropertyWithComputedProperty
// ReSharper disable ArrangeAccessorOwnerBody
// ReSharper disable MergeIntoLogicalPattern
// ReSharper disable EventNeverSubscribedTo.Global
// ReSharper disable ConvertToPrimaryConstructor
// ReSharper disable UnusedAutoPropertyAccessor.Global
// ReSharper disable UnusedType.Global
// ReSharper disable UnusedMemberInSuper.Global
// ReSharper disable UnusedParameter.Global
// ReSharper disable EmptyConstructor
// ReSharper disable EmptyNamespace
// ReSharper disable NotAccessedField.Local
// ReSharper disable UseCollectionExpression
// ReSharper disable CollectionNeverQueried.Local
// ReSharper disable AppendToCollectionExpression
public class Program
{
public static void Main()
{
// GUIA DE C#!!!!!!!111!1!1!1
// Esteve Autet Alexe
// memw.es
/*
VARIABLES Y TIPOS
En C# existen varios tipos distintos llamados tipos primitivos, los cuales
supuestamente no hay forma de replicar como por ejemplo los números, caracteres...
Esos tipos forman parte del comportamiento base de c#, para diferenciar estos
tenemos algo llamado `keywords`, que su traducción literal es palabras clave.
Para diferenciar estas podemos ver que estas se escriben en minúscula, y los
tipos normales en pascal case, o sea separando las palabras por mayúsculas DeEstaForma.
Cabe recalcar que los tipos que tienen palabra clave, también se pueden referenciar con
el tipo en pascal case, por ejemplo, puedes referenciar `string` como `String`, ya que realmente
`string` es solo otro nombre para `String`, e internamente el lenguaje lo convierte,
pero que el lenguaje lo haga, no significa que lo debas hacer tú.
En conclusion, para referenciar un tipo primitivo, siempre usa la palabra clave, y para
referenciar uno que no sea primitivo, usa pascal case.
Pero, ¿cuáles son los tipos primitivos?
`bool, byte, sbyte, char, decimal, double, float, int,
uint, ning, nuint, long, ulong, short, ushort, object, string, dynamic`.
Los 3 últimos tipos, más que primitivos son solo palabras clave para un tipo reproducible.
Una vez sabemos qué tipos tenemos disponibles, podemos proceder a usarlos. La forma más básica
de usarlos es con variables.
La sintaxis de una variable es la siguiente.
` tipo nombre = valor; `
por ejemplo, para hacer una variable de tipo string que contenga "Hola Mundo", haremos lo siguiente.
*/
string variable1 = "Hola Mundo";
/*
Bien, eso es una variable, pero... ¿Y el valor?
En la mayoría de lenguajes de programación hay un concepto llamado literales, en donde
cada tipo primitivo tiene su forma literal de representarse, asi teniendo
una forma de representar los tipos primitivos objetivamente.
¿Y cuáles son esas formas?
Para representar una string debemos usar `""` y dentro de esas comillas poner nuestro texto,
como `"Hola mundo!"`.
Para representar un número entero como `byte, int, long, short, uint, ulong, ushort, nint y nuint`,
podemos escribir literalmente el valor, y c# lo convertira automáticamente por nosotros.
*/
int variable2 = 10;
long variable3 = 10;
/*
La principal diferencia entre estos tipos es el tamaño, esto depende también del sistema operativo.
Tabla de tamaños de tipos de c#
https://learn.microsoft.com/es-es/dotnet/csharp/language-reference/builtin-types/integral-numeric-types
Para representar decimales, es lo mismo, pero como la `, ` sirve para separar argumentos,
elementos y otros, sería ambiguo usarla para representar puntos decimales, para eso usamos el `. `.
Los tipos decimales son los siguientes, `double, float y decimal`, las representaciones literales
de estos tipos son sencillas, pero c# no las convierte automáticamente, por lo que tenemos
que decirle explicitness como hacerlo.
*/
// para el `decimal`, se debe convertir explícitamente, se explicará posteriormente.
decimal variable4 = (decimal)10.5;
// para el `float`, poniendo una `f` al final es suficiente.
float variable5 = 10.5f;
// y como los decimales por defecto son `double`, a este no se le debe aplicar conversion.
double variable6 = 10.5;
/*
En cuanto a tipos booleanos estos tienen 2 posibles valores "true" o "false",
siendo su traducción literal; verdadero y falso.
Los casos de uso de este tipo son cuando quieres representar la veracidad de algo, por ejemplo:
¿Es este usuario un administrador?
A pesar de que de forma normal estas variables no se declaran directamente, si no, a traves de
una expresión, pero esto lo veremos luego.
*/
bool variable7 = true;
bool variable8 = false;
/*
Una variable que ya existe, también puede ser reasignada, una vez se haya declarado una vez,
como en este caso variable8, podemos reasignarla, pero solo con valores del mismo tipo.
*/
variable8 = true;
/*
Para los literales numéricos, existen varias formas de expresarlos,
por ejemplo, podemos expresarlos de forma binaria prefijándolos con 0b, o de forma
hexadecimal con 0x.
Cabe mencionar que indiferentemente del literal, el valor es el mismo,
y se manipula de la misma forma, eso sirve para representar memoria
u otros casos de uso más específicos donde representar nuestros números
con otra base sea semánticamente mejor visto.
*/
int variableBinaria = 0b00101010; // esto nos da 42.
int variableHexadecimal = 0x2A; // esto también nos da 42.
/*
Por otro lado, los literales numéricos en C# pueden ser separados con `_`,
asi pudiendo separar decimales, o si estamos representando binario separar bytes.
*/
int cienMil = 1_000_00;
int cienMilBinario = 0b_1_10000110_10100000;
/*
En c# también tenemos variables constantes, por ejemplo PI, pi sería una constante, nunca cambia.
Para hacer eso simplemente ponemos `const` detrás del tipo en la declaración de una variable,
la diferencia es que una variable constante no puede tener como valor nada que no sea un literal.
Al hacer una variable constante, tampoco la podemos reasignar.
*/
const double pi = 3.141592653589793;
/*
TIPOS Y OPERADORES
En c# existe un concepto llamado operadores, hay distintos tipos de operadores que podemos usar.
Operadores unarios, estos operadores se aplican a un solo valor, su objetivo es modificar este mismo.
Operadores binarios, estos se aplican entre 2 valores, por ejemplo para sumar, restar, multiplicar o dividir.
Luego existen subcategorías de estos, por ejemplo operadores de asignación, de comparación, aritméticos...
Pero esto también se puede relacionar a tipos, ya que en c# se hace de esta forma, en los lenguajes
como C++ y C#, tú puedes definir el comportamiento de los operadores en tus tipos, pero vamos a ver
eso más adelante.
Tal y como tú puedes definir tus propios operadores para tus clases, c# ya hizo eso en sus clases.
Ejemplo de aritméticos
1 + 1; suma
1 / 1; division
1 * 1; multiplication
1 - 1; resta
1 % 1; residuo de division
Ejemplo de operadores condicionales
10 > 5; 10 es mayor que 5.
10 < 5; 10 es menor que 5.
10 <= 5; 10 es menor o igual a 5.
10 >= 5; 10 es mayor o igual a 5.
10 == 5; 10 es igual a 5.
10 != 5; 10 no es igual a 5.
Ejemplo de operadores lógicos
ver https://es.wikipedia.org/wiki/Puerta_l%C3%B3gica
ver https://learn.microsoft.com/es-es/dotnet/csharp/language-reference/operators/boolean-logical-operators
1 & 1; verdadero AND verdadero (1).
1 | 1; verdadero OR verdadero (1).
1 ^ 1; XOR true true, o sea falso (0).
!true; NOT true, o sea, false.
true && true; AND condicional, compara booleanos en vez de números,
true || true; OR condicional, compara booleanos en vez de números
Cabe destacar que puedes usar booleanos en AND, OR y XOR, ya que c# los convierte
implícitamente a números siguiendo esa regla de que se pueden declarar operadores
personalizados a tus clases.
Ejemplo de operadores de asignación
Los operadores de asignación, como expresión retornan el resultado de la operación,
al igual que cualquier otro operador te retorna el resultado de su evaluación.
a = 10; la variable a, ahora es igual a 10.
a++; permite aumentar a por 1, siendo lo mismo que `a = a + 1`.
a--; permite disminuir a por 1, siendo lo mismo que `a = a - 1`.
++a; permite aumentar a por 1, primero aumentando el número y luego devolviendo el resultado.
--a; permite disminuir a por 1, primero disminuyendo el número y luego devolviendo el resultado.
a += 10; permite aumentar a por cualquier número.
a -= 10; permite disminuir a por cualquier número.
En la mayoría de operadores tal y como puedes hacer +=, también puedes aplicar lo mismo a esos,
por ejemplo el or, puedes hacer |=.
Los operadores de asignación solo aplican a variables, ya que tú no puedes asignar una expresión.
(1 + 1) = (1 + 1); eso no es válido, porque (1 + 1) devuelve un resultado temporal,
no una referencia a memoria válida.
Cabe destacar que la mayoría de operadores aplican a más tipos aún que la mayoría de ejemplos,
sean con números. Por ejemplo las string.
"Hola " + "mundo"; sería "Hola mundo".
EXPRESIONES COMPUESTAS
Las expresiones son conjunto de operadores en cadena que resultan en un valor único,
por ejemplo:
1 + 1 * 5
El resultado de esta expresión es 6, al igual que las mates básicas, C# sigue PEMDAS para
la aritmética simple, lo único que añade más operadores asi incluyendo a los operadores no aritméticos
ver: https://learn.microsoft.com/es-es/dotnet/csharp/language-reference/operators/,
pero al igual que las matemáticas, nosotros podemos sobreescribir este comportamiento, cerrando
nuestra expresión u operación entre paréntesis (), por ejemplo:
(1 + 1) * 5
El resultado de esta expresión es 10, ya que nosotros hemos sobreescrito el orden de operaciones.
GESTION DE CONDICIONALES
En c# si queremos evaluar una expresión antes de ir a una parte de código u otra, podemos usar
las siguientes palabras clave `if, else`.
*/
if (true) ;
/*
La sintaxis del `if` en c#, es poner la palabra clave, seguido de paréntesis, y dentro de estos
un valor booleano, de forma normal, en un if se pone una expresión que su evaluación
devuelva un booleano, luego entre corchetes ponemos el código que se ha de ejecutar
cuando la expresión sea verdadera.
Por ejemplo, podemos comprobar la edad de un usuario.
*/
int edadUsuario = 16;
if (edadUsuario >= 18) // si la edad del usuario es mayor o igual a 18
{
// en una discoteca podríamos permitir el paso a este usuario.
}
else // si no
{
// en una discoteca podríamos denegar el paso a este usuario.
}
/*
Estos 2 se pueden combinar, asi formando un else if, o un condicional complejo.
Este se evaluaría cuando la condicion de arriba se evaluó como falsa.
Sabiendo que hay discotecas con restriction de edad con límite inferior y superior,
podemos aplicar el siguiente ejemplo.
*/
if (edadUsuario > 20) // si la edad del usuario es mayor que 20
{
// no dejamos pasar a mayores de 20
}
else if (edadUsuario < 18) // si la edad del usuario es menor a 18
{
// tampoco lo dejamos pasar
}
else // si la edad del usuario es entre 18 y 20.
{
// lo dejamos pasar
}
/*
La anterior demostración fue un ejemplo de `else if`, de normal eso se sería con una
expression compleja, como explicado anteriormente.
*/
// si la edad del usuario es mayor o igual a 18 y menor o igual a 20
if (edadUsuario >= 18 && edadUsuario <= 20)
{
// lo dejamos pasar
}
else // si no
{
// no lo dejamos pasar.
}
/*
OPERADOR TERNARIO
El operador ternario es una forma de evaluar una expresión booleana y devolver
un resultado u otro resultado dependiendo de la veracidad de su evaluación.
La sintaxis de este es la siguiente:
(condicion) ? (verdadero) : (falso)
Debemos tomar eso como una expresión, toma el siguiente ejemplo
*/
string pase = edadUsuario >= 18 ? "puede pasar" : "no puede pasar";
/*
Entonces, si la edad del usuario es 18, el valor de `pase` será "puede pasar",
pero al contrario, si este es 17, el valor de `pase` será "no puede pasar".
ÁMBITOS
Ahora que se han introducido los corchetes, podemos llamarlos cuerpos,
cualquier variable o referencia declarada dentro de un cuerpo, no podrá
ser llamada ni referenciada fuera de este, pero si en cuerpos que estén
dentro de este, como si fuese una jerarquia.
También se puede declarar un cuerpo sin ninguna palabra clave, estos son
realmente un componente separado de cualquier cosa, y es como "ejecuta lo que
haya aquí dentro" como algo que engloba un conjunto de expresiones y declaraciones
*/
{
int variableInterna = 0;
}
// no podemos referenciar variableInterna aquí.
/*
CADENAS
Las cadenas (o array) en c#, sirven para almacenar una colección de valores,
por ejemplo, una lista de números, la cual posteriormente queramos ordenar,
o procesar de cualquier otra forma, o en otro caso, los usuarios de un servidor
de discord, los cuales podemos contar, cambiar o procesar de cualquier otra forma.
Para declarar una cadena se hace con tipo[], y el literal de una cadena también es [],
en versiones anteriores el literal de una cadena era {}, pero eso, cambio en c# 12.
*/
int[] variable9 = [1, 2, 3, 4, 5];
/*
Las cadenas son estructuras complejas, pero pueden guardar cualquier valor, de cualquier tipo,
la única desventaja es que son de tamaño fijo.
Para acceder al valor de una cadena utilizamos el operador de [], por ejemplo,
en la variable que hemos declarado, si procesamos la expresión `variable9[0]` obtendremos
el valor 1, ya que es el primero de la lista, los indexes empiezan a contarse por 0.
Como en c# es un lenguaje orientado a objetos, significa que todas las estructuras
son objetos, si no, la mayoría, entonces las cadenas tienen propiedades, métodos...
Una propiedad que vamos a usar ahora sin entrar muy a fondo en estas es la propiedad
Length en una cadena, para acceder a ella utilizamos un `. `.
*/
int longitudVariable9 = variable9.Length;
/*
La longitud en una cadena se empieza a contar por 1, al contrario que los índices,
este concepto se llama "0-Based indexing" o "1-Based indexing".
El valor de longitudVariable9 es 5, ya que nuestra cadena tiene 5 valores.
A las cadenas también se les puede asignar, porque el operador de [] devuelve una referencia
a memoria y no un valor.
*/
variable9[0] = 0;
/*
Desde C# 8, tenemos un concepto que son índices complejos y rangos.
Por ejemplo, la expresión `variable9[2..4]` devolvería una subcadena del índice 2 al 3, ignorando el 4.
En este caso `[3, 4]`, ya que los índices empiezan por 0 como mencionado anteriormente
ver https://learn.microsoft.com/es-es/dotnet/csharp/language-reference/proposals/csharp-8.0/ranges.
También tenemos otra forma de instanciar cadenas, que es especificando su tamaño, de esta forma
estaríamos iniciando una cadena con el valor default del tipo de la cadena.
*/
int[] variable91 = new int[10]; // A esta cadena le caben 10 números.
/*
Para asignar valores a esta cadena lo haríamos como anteriormente explicado
*/
variable91[0] = 1;
/*
En este caso, el valor por defecto de `int` es 0, más adelante se hablará de eso,
entonces podemos asumir que si quisiéramos tomar el valor a un index
que no hemos asignado recibiríamos 0.
TUPLAS
Las tuplas en C# son un tipo de variable que podemos usar para almacenar varios valores,
por ejemplo
*/
// Declaramos una tupla con el tipo (string nombre, int edad) y la asignamos al valor ("juan", 18)
(string name, int age) tuple = ("juan", 18);
// Accedemos a los valores de la tupla.
string name = tuple.name;
int age = tuple.age;
/*
Las tuplas también soportan de-estructuración de variables, en este caso
no hubiera sido necesario poner `tupla` como nombre ahí.
*/
(string kind, bool hasOwner) = ("dog", true);
/*
Ahora somos directamente capaces de referenciar `kind` y `hasOwner`.
Si no les ponemos nombre por defecto esos seran `Item1`, `Item2`...
Y la cantidad de valores que tengamos, a las tuplas no se las puede indexar.
*/
// Creamos una tupla con nombre y le asignamos los valores ("hello", "bye")
(string, string) tuple2 = ("hello", "bye");
// Accedemos a los valores de la tupla.
string hello = tuple2.Item1;
string bye = tuple2.Item2;
/*
Las tuplas se las puede considerar una estructura compleja, ya que
estas son una composición de datos.
En el caso de las tuplas no pueden ser indexadas de forma normal,
solo podemos acceder a los valores directamente a traves de
sus respectivos nombres como mostrado anteriormente,
las tuplas pueden tener la cantidad de elementos que se desee.
Para obtener más información sobre tuplas
ver: https://learn.microsoft.com/es-es/dotnet/csharp/language-reference/builtin-types/value-tuples
BUCLES E ITERADORES
En programación, hay un concepto que se llama iteración, que significa moverse por una serie
de valores conjuntos.
Un bucle puede no solo servir para iterar, sino que también para repetir una accion multiples veces.
Para la tarea de repetir una accion, o hacer una iteración compleja, podemos usar `while, do while o for`
La sintaxis de un loop `for` es la siguiente.
*/
// Declaramos una variable como i, que tendra el valor de 0
// comprobamos que `i` es menor que 10, si es asi, ejecutamos el contenido
// del bucle, si no, continuamos abajo del `}`, si lo hemos ejecutado,
// aumentamos `i` por 1, con la expresión `i++`
for (int i = 0; i < 10; i++)
{
// contenido a repetir.
}
// también tenemos el ejemplo de una iteración, que sería desde 0, a la longitud de la cadena.
for (int i = 0; i < variable9.Length; i++)
{
int variable10 = variable9[i]; // podemos acceder y procesar ese número.
}
/*
Luego tenemos otro concepto que es el `while` o mientras, la sintaxis de este
es, la palabra clave, seguida de un paréntesis con la condicion, posteriormente corchetes para
definir que es lo que queremos hacer con él.
*/
int limite = 0; // definir limite como 0
while (limite < 10) // mientras limite sea menor a 10
{
limite++; // aumentamos limite por 1
}
/*
El `do while` es similar, pero antes de evaluar la primera condicion,
vamos a ejecutar el bloque de código.
*/
limite = 0; // redefinimos límite como 0
do
{
limite++; // aumentamos límite por 1
} while (limite < 10); // mientras limite sea menor a 10
/*
Y posteriormente, tenemos el `foreach`, este sirve para iterar de forma simple
cualquier tipo de colección, en este ejemplo, vamos a iterar `variable9`
*/
// por cada valor (i) en `variable9`
foreach (int i in variable9)
{
// procesamos `i` como sea, `i` no se puede modificar
// mientras estemos iterando, variable9 tampoco se puede modificar.
}
/*
También podemos salir de un bucle usando `continue` o `break`,
por ejemplo salir si los números es 5, o no procesar los números pares.
En el caso del `continue`, este sigue a la siguiente iteración, por ejemplo,
en un foreach, simplemente seguiria iterando, sin tener en cuenta el resto
de instrucciones dentro del cuerpo.
*/
foreach (int i in variable9)
{
if (i % 2 != 0) // si el número és par
continue; // seguimos sin hacer lo que hay en el resto del código
// haríamos cualquier otra cosa.
}
/*
También disponemos del break, que sale completamente del loop,
al verse esta instrucción, va a completamente dejar de iterar,
por ejemplo, si queremos saber si hay un elemento dentro de una lista,
una vez lo encontremos, no hará falta seguir comprobando, podríamos salir
del bucle.
*/
int numeroABuscar = 3; // queremos saber si existe el numero 3.
bool existeEnLaCadena = false; // definimos el resultado como false, ya que aún no lo sabemos.
foreach (int i in variable9) // iteramos todos los valores de `variable9`
{
if (i != numeroABuscar) // si el valor actual no es igual que el numero a buscar
continue; // continuamos.
existeEnLaCadena = true; // si lo és, reasignamos el resultado como true.
break; // salimos del bucle para no seguir comprobando.
}
/*
SWITCH
una expresión `switch`, es lo mismo que un if, pero en mayor escala,
por ejemplo, si necesitamos evaluar el valor de algo, y cubrir
muchos valores posibles, como por ejemplo, una máquina expendedora.
Si se selecciona un número, se debe obtener una bebida. Y obtenemos esa
bebida basándonos en el número.
La palabra clave `case`, `break`, `when` y `default`, también se encuentran
presentes en la estructura del `switch`
*/
int entradaBebida = 5; // el usuario selecciono la bebida 5
switch (entradaBebida) // tomamos la entrada como parámetro a comparar
{
case 1:
// dispensaríamos cafe.
break; // salimos del switch
case 2:
// dispensaríamos agua.
break;
case 3:
// dispensaríamos cola.
break;
case 5:
case 6: // si és cualquiera de los 2
// dispensar otra bebida.
break;
case 7 when edadUsuario >= 18: // si entrada es 7 y la edad del usuario es mayor o igual a 18
// dispensar alguna bebida alcohólica.
break;
default: // si ninguna de las anteriores bebidas es introducida
// mostraríamos mensaje de error.
break;
}
/*
ENTRADA Y SALIDA DE DATOS POR PARTE DEL USUARIO
para interactuar con el usuario de forma simple, c# en las aplicaciónes de consola,
incluye una clase estática llamada `Console`, todos los conceptos de clases también
están explicados aquí.
Por el momento nos quedamos en que existe una clase que se llama Console,
la cual podemos referenciar sin importar ni incluir nada.
Un ejemplo que seguro que has visto en algún sitio es el famoso "Hola mundo".
Dentro de console, existe un método estático llamado WriteLine, este imprime
una línea en la consola, una vez acabada esa línea, acaba con un terminador de línea.
*/
Console.WriteLine("Hola mundo!");
/*
Dentro de la clase `Console` también hay un método estático llamado `Write`,
este hace lo mismo, pero sin acabar en una nueva línea.
Un ejemplo de usabilidad de este método es a la hora de pedir por un número
al usuario.
*/
Console.Write("Introduce un número: ");
/*
En este caso, el usuario podria introducir el número y quedaria asi:
Introduce un número: 10
Al contrario, si utilizásemos el método `WriteLine`, la cosa se vería asi:
Introduce un número:
10
Creo que estamos de acuerdo en que el primer ejemplo queda mejor.
Mucho hablar de entrar datos, pero, ¿Cómo lo hacemos?
Para permitir al usuario que nos entre datos, usamos el método `ReadLine`,
al utilizar este, automáticamente se imprimira una nueva línea, y aplica la
misma lógica con el método `Read`, en este no se imprimiria una nueva línea.
El método `ReadLine` devuelve un resultado, lo podemos tomar como una expresión,
al contrario de `WriteLine` que no lo hace, aprenderemos a diferenciar eso más tarde.
*/
// guardamos el resultado de `ReadLine` en una variable
string variable11 = Console.ReadLine();
/*
En este ejemplo, variable11, tiene el resultado de lo que sea que haya
introducido el usuario por consola.
Aquí empezaría a tener sentido el tema `programación`, luego deberíamos validar
esos datos, procesarlos y como programa, devolver una salida por donde sea que estemos
interactuando con el usuario, en este caso por la consola.
Ahora podemos dar un ejemplo que tenga más sentido. En donde podamos pedir
al usuario si quiere hacer algo o no.
*/
bool entradaValida = false; // definimos que la entrada no es válida, ya que aún no la hemos validado.
while (!entradaValida) // mientras la entrada NO sea válida.
{
Console.Write("Quieres salir del programa? "); // hacemos la pregunta
string continuarPrograma = Console.ReadLine(); // tomamos la respuesta del usuario
if (continuarPrograma == "N") // si es N, o sea no
{
return; // salimos del programa
}
else if (continuarPrograma != "S") // si no es S o sea si
{
Console.WriteLine("Opción invalida"); // comunicamos opción inválida.
continue; // volvemos a empezar el bucle.
}
entradaValida = true; // ponemos que entradaValida sea verdadero.
}
/*
Como hemos podido ver en algunos de los anteriores ejemplos, en la programación
siempre somos negativistas. Si no hemos comprobado algo, la validez de eso siempre
será inválida, sea lo que sea que haya introducido el usuario, PUEDE Y MUY POSIBLEMENTE
ser incorrecto.
VALORES NULOS I VALIDACIÓN
En C#, cuando queremos representar una variable vacía, podemos inicializarla como null,
null es un literal que apunta a la dirección de memoria 0, ahí no hay ningun valor.
El funcionamiento de null en c#, funciona a traves de (una envoltura) o wrapper en inglés,
esta es una clase que administra el hecho de que puedas tener una variable como null
o como un valor, a esta se le conoce como `Nullable<T>`.
En C# se ha implementado el funcionamiento de esta clase como parte del lenguaje, y
aunque quisieras no podrías replicarla sin modificar el código del lenguaje.
Para envolver un tipo con `Nullable<T>` se debe especificar un símbolo de pregunta al final
del tipo, asi como `string?`, eso se convertiria en un `Nullable<string>`, a eso se le
llama un tipo genérico, que se van a cubrir más adelante.
*/
string? variable12 = null;
/*
Si esta no se envuelve obtendremos un error de runtime a la hora de asignar el valor a la
variable. Eso significa que el programa se parara una vez empezado.
Para comprobar si un valor es nulo o no, podemos usar un `if`
*/
if (variable12 != null)
{
// hacer algo de lo que dependemos que la variable no sea null.
}
/*
Si nosotros intentamos hacer cualquier tipo de operación en un valor nulo, también
obtendremos un error de runtime, el famoso:
"Object reference is not set to an instance of an object".
De manera que antes de procesar cualquier tipo con la anotación nullable (?)
deberíamos comprobar si el valor que contiene la referencia, no es nulo.
Nuestro editor de normal antes de procesar una referencia que puede ser nula
nos va a avisar de que puede serlo, y nos dice que debemos comprobarlo, pero nosotros
podemos asertar que ese valor no es nulo, por ejemplo si se ha hecho una comprobación
de otra forma, o con métodos externos, o por algún estándar o entorno sabemos que no lo es.
Este es el operador de símbolo de exclamación, el mismo que se usa para negar un valor
booleano, pero en este caso se usa para asertar que un valor no será nulo si es que
se pone al final.
Por ejemplo, por standard, el método `ReadLine` de `Console` retorna null en caso
de que estemos ejecutando la application en un entorno donde leer de la consola
no sea compatible, como por ejemplo, con ASP.NET, al ser una app de navegador,
si por alguna razón intentamos usar el método en la parte del frontend,
este nos devolvería null.
Pero en este caso, como estamos en un sistema operativo, y tenemos una terminal que
en la que podemos escribir y leer, este nunca retornara null.
Entonces para asertar eso usamos el operador (!) al final.
De esta forma, lo guardamos en una variable del tipo normal, sin el wrapper, y
nos ahorramos validaciones extras.
*/
string nuncaNull = Console.ReadLine()!;
/*
También podemos encadenar una operación a un valor nulo de forma condicional.
Tenemos el operador (.?) que nos sirve para esto, este es básicamente el operador
de acceso a un método o propiedad, pero condicionalmente, si el valor anterior al
operador es nulo, toda la expresión se convertira en nula automáticamente.
Por ejemplo, en el caso de que `ReadLine` nos devolviese null, si queremos
convertir el texto retornado por `ReadLine` a minúsculas, podemos usar
el método `ToLower` de `string` condicionalmente.
*/
string? inputEnMinus = Console.ReadLine()?.ToLower();
/*
En este caso, si el valor retornado por `ReadLine` es nulo, el método `ToLower`
no se ejecutará, y toda la expresión se evaluará como nula, resultando asi
que inputEnMinus sea null, sin darnos una excepción de referencia nula.
CONVERSION DE TIPOS, VALIDACIÓN Y POLIMORFISMO
En C# existe un concepto llamado `cast`, que permite convertir un tipo a otro, dentro
de la jerarquia de clases en donde este está declarado, por ejemplo, no puedes
hacer `cast` de un int a un string, porque eso no tiene sentido, no hay
forma de convertir "a" a un número, semánticamente eso se hace con un método
de `parse`, la mera diferencia es que se ejecuta un método estático dentro de un tipo
en vez de convertir una instancia.
Para hacer `cast` a una instancia de un tipo a otro tipo, primero ese tipo debe de alguna
forma estar relacionado en la jerarquia de clases, el sistema de tipos se cubre
más adelante, pero la explicación oficial de microsoft ante eso es
ver: https://learn.microsoft.com/es-es/dotnet/csharp/fundamentals/types/
ver: https://learn.microsoft.com/es-es/dotnet/csharp/programming-guide/types/casting-and-type-conversions.
Para convertir de un tipo a otro por `cast` se hace a traves de los paréntesis,
delate de una expresión pones (tipo), para convertirlo, si es que la conversion
es válida, no te mostrara un error.
*/
// convertimos un entero a un char, como el código ASCII para 'a' es 65
// esto nos resultaria en 'a' dentro de una variable de tipo char.
char a = (char)65; // a en ASCII.
/*
También hay otra forma de hacer `cast`, con la palabra clave `as`, esta te permite
probar de hacer cast, o devuelve null si es que el cast fue imposible, en casos de
polimorfismo en donde no sepas que pueda ser el objeto, si te dan un tipo demasiado base
como sería `object` el cual es la base de la jerarquia de clases
como información de una variable, puedes usar `as` para hacer `cast` de forma segura.
Pudiendo asegurarte de si puedes hacer `cast` a un valor antes de procesarlo, ya que
este te devolverá null antes en vez de darte un error.
Esta conversion se debe hacer explícitamente con tipos nullables, porque es de la forma
en la que sé válida la conversion.
*/
object b1 = 10;
int? b2 = b1 as int?;
/*
En la conversion de tipos también existe el concepto de "parsing",
el cual de normal, mediante una funcion, podemos convertir cualquier cosa a
otro tipo, por ejemplo convertir una string, comprobando si esta solo contiene
números, y dando un objeto de otro tipo como resultado.
Para hacer eso, cada tipo tiene un método diferente de convertirse,
por ejemplo, todos los tipos numéricos pueden ser obtenidos con base en
un string, o también se puede obtener un booleano a partir de un número o
representación de string literal.
Por ejemplo para convertir tipos numéricos, utilizaremos los respectivos
métodos `Parse` de cada tipo numérico.
*/
int c1 = int.Parse("1000"); // esto nos dara el número 1000.
double c2 = double.Parse("10.5"); // esto nos dara el número 10.5
float c3 = float.Parse("10.5"); // esto nos dara el numero 10.5 en forma de float.
// para los booleanos pasa lo mismo
bool c4 = bool.Parse("true"); // esto nos dara true.
bool c5 = bool.Parse("false"); // esto nos dara false.
/*
MANEJO DE ERRORES, Y GENERACIÓN DE ESTOS
Para manejar errores, siempre es importante que validemos cualquier entrada
que tengamos del usuario o de cualquier servicio de terceros hacia nuestro programa,
pero si por cualquier razón eso se nos hace imposible, entonces podemos usar
la cláusula `try` y `catch`, esta nos permite como su nombre en inglés indica
probar de ejecutar un código, y capturar un error si es que nos lo encontramos.
*/
try
{
// código que tiraria un error.
}
catch (Exception error)
{
// Código que nos permitiría dar un mensaje de error al usuario.
// `error` es una variable que podemos usar para obtener información del error.
}
/*
En este caso estamos capturando un error de tipo `Exception` que es el tipo base
para cualquier excepción que nos encontremos en el lenguaje.
Esto entraria en la parte de herencia y tipos en profundidad, pero por ahora
nos podemos quedar con que todos los tipos de excepción heredan de `Exception`.
Siendo ese el caso, queda más claro que puedes capturar varios tipos de error distintos,
por ejemplo, si quieres capturar un `FormatException` y un `NullReferenceException` al mismo
tiempo, puedes poner varias cláusulas de `catch`.
*/
try
{
// Código que debamos probar.
}
catch (FormatException errorDeFormato)
{
// Error como una excepción de formato.
}
catch (NullReferenceException errorDeReferencia)
{
// Error como una excepción de referencia
}
catch
{
// Cualquier otro tipo de error (sin acceso a la referencia de este mismo)
}
/*
Como vimos en el ejemplo anterior, también podemos capturar cualquier error
sin una referencia a él.
También tenemos la posibilidad de capturar un error condicionalmente,
al igual que el `switch`, aquí `when` también está disponible, con la
única diferencia que aquí se debe especificar entre paréntesis.
*/
int? numeroNulo = null;
try
{
// Código que debamos probar.
}
// Solo se capturaria la excepción si numeroNulo es 5,
// implícitamente estamos comprobando que no es nulo, ya que 5 != null.
catch when (numeroNulo == 5)
{
// Código que deba ir en la captura de un error, en este caso sin referencia al error.
}
/*
El `when` también está disponible si capturamos un tipo de excepción en específico.
Para finalizar con el `try catch` tenemos la cláusula `finally`, esta se ejecutará
se haya tirado un error o no, y sirve para limpiar recursos, una explicación más a detalle,
ver: (inglés) https://stackoverflow.com/questions/9687849/what-is-the-point-of-finally-in-a-try-catch-except-finally-statement
*/
try
{
// Código que debamos probar.
}
catch
{
// Captura de error.
}
finally
{
// Esto se ejecuta se haya tirado o no un error.
}
/*
Si hay algún tipo de excepción que no hemos capturado, esta se tirara y acabara el programa.
Para generar un error por nuestra cuenta, podemos usar la palabra clave `throw`, esta nos
permite acabar el programa con un error, esta solo debe ser usada cuando el código
que estás haciendo está orientado a B2B, como otros programadores.
Para usarlo deberemos generar una nueva instancia de una excepción, o cualquier clase que
herede de una.
Para generar una nueva instancia usaremos la palabra clave `new`, y pondremos el nombre
de una clase como si estuviésemos ejecutando una funcion, como funcionan las instancias
en profundidad está escrito en el curso posteriormente.
En el caso de `Exception` el constructor solo tiene como parámetro una string,
*/
throw new Exception("Ocurrió un error.");
/*
Eso de aquí saldría del programa mostrando un error con la línea donde ocurrió, y el
texto que introducimos.
*/
} // A partir de aquí, las expresiones que se mostraron anteriormente, no aplican, estas
// deben estar dentro de un método.
/*
MÉTODOS
Hemos visto que existen métodos para muchas cosas, por ejemplo `Parse` para cambiar
tipos de datos, o `WriteLine` para escribir en la consola... ¿Pero, Y como declaramos
nuestro propio método entonces?
Bien para esto primero debemos familiarizarnos con unos cuantos conceptos.
Modificadores de acceso, de forma un poco abstracta, un modificador
de acceso es para definir que tiene acceso a tu método y que no,
por ejemplo, los modificadores de acceso son `public`, `private`, `protected`,
`internal` y `file`
`public` significa que todos pueden acceder en cualquier sitio.
`private` significa que solo el mismo contexto es capaz de acceder al miembro.
`protected` significa que solo el mismo contexto y los que heredan este mismo son capaces de acceder.
`internal` significa que solo el mismo assembly puede acceder a esta, más adelante hablaremos de assembly.
`file` solo se puede usar en algunos contextos, y significa que solo se puede acceder al miembro desde
el mismo archivo en donde se declara.
La declaración de acceso del miembro que contiene el miembro que estemos declarando precede
sobre la declaración de acceso de este mismo, siendo asi, si nosotros hacemos
una clase internal y un miembro público, el miembro será internal (público para
los que tengan acceso a esa clase).
También cabe mencionar que si queremos que una clase sea internal, y hacemos un método
completamente publico con la clase que lo contiene también siendo pública,
esto nos daria un error de acceso, ya que el tipo del parameter siempre debe
ser 100% accesible para el contexto objetivo.
Un ejemplo de los modificadores de acceso sería que si el método `WriteLine` en `Console`
fuese privado, nosotros no tendríamos forma de llamarlo.
Otro concepto importante es si un método es estático, esto se define con la palabra clave
`static`, y significa que cuando un método es estático, no es parte de una instancia, anteriormente
mencionamos los objetos de una forma muy abstracta, y vimos 2 métodos distintos:
`Parse` en tipos numéricos, una característica que los denota es que se llaman a traves del tipo como
`Tipo.Parse()`, cuando el método se llama desde el tipo, significa que es estático,
A diferencia de otros métodos como `ToString` en `object` o `ToLower` en `string`, esos no
son estáticos, ya que se llaman a traves de un objeto.
`int.Parse()` // estático
`"HOLA".ToLower()` // no estático
También debemos familiarizarnos con el retorno de valores en funciones, anteriormente
vimos que funciones como `int.Parse()` devuelven un numero basado en un string como
parte de una expresión.
Tal y como (1 + 1) ex una expresión que devuelve 2, si tuviésemos una funcion llamada `Suma`
la cual sumase 2 números, entonces `Suma(1, 1)` también devolvería 2.
También hay un concepto llamado parámetros, que es la parte que hay entre paréntesis en la llamada
de una funcion, por ejemplo, una funcion como `Sum` donde le pasamos 2 números separados
por una `, `, eso contaria como 2 parámetros, nosotros en nuestras funciones podemos
definir cuantos parámetros queremos, de qué tipo y que significan, por decirlo de alguna
forma, son variables que obligas a la persona que use la funcion a llenar por ti.
La sintaxis de una funcion es la siguiente:
(public|private|protected) (static|<none>) (tipo retorno) (nombre)((parámetros)) ({cuerpo})
Por ejemplo, la funcion Suma se declararia de la siguiente forma:
*/
// Ponemos los modificadores, el nombre llamado `Suma` con los parámetros y su tipo correspondiente.
// A esto se le conoce como signatura de una funcion o método.
// Si deseamos que un parámetro tenga un valor por defecto, podemos poner `=` al final y un valor
// de esta forma, C#, al llamar la funcion o método, no nos va a pedir ese parámetro
// y podremos llenarlo de forma opcional.
public static double Suma(double numero1, double numero2)
{
// Esta variable no es necesaria, pero es una demostración de que
// puedes poner código que se ejecuta aquí.
double resultado = numero1 + numero2;
// Para devolver el valor, usaremos la palabra clave `return`, la expresión que
// hay a la derecha, ha de ser del mismo tipo que la signatura de la funcion.
// Si no queremos devolver ningun resultado, podemos especificar
// que el tipo de retorno sea `void`, siendo su traducción al Español: vacío.
return resultado;
}
/*
En cuanto a zonas donde puedes escribir y no escribir código, existen el nivel superior,
o sea donde aún no has escrito nada, ahí se pueden declarar espacios de nombre,
clases, delegados, enums y poco más.
Dentro de una clase, puedes declarar métodos, propiedades y campos, también puedes
declarar clases, delegados, enums, eventos... Pero el anidar tipos si es que no
son estáticos, no es una práctica común.
Dentro de los métodos, puedes escribir código, o sea, todos los ejemplos que
se mostraron anteriormente, también puedes escribir funciones dentro de estas,
la única diferencia que sin modificadores de acceso, ya que están dentro de
otra funcion, y lo único que puede acceder a ellas es la funcion misma,
siguiendo la lógica de los ámbitos. Eso sí, si una funcion es estática
esta no podrá acceder a las variables del método que la contenga.
La diferencia entre funciones y métodos, es que los métodos se encuentran
dentro de una clase, y las funciones dentro de un método, pero siguen la misma
lógica, y son lo mismo, lo único que cambia es el ámbito donde se puede acceder
a ellas.
Nosotros fuimos capaces de declarar ese método, porque estamos dentro de
la clase `Program`.
PROPIEDADES Y CAMPOS
Las clases, aparte de dejarnos declarar métodos, también nos dejan declarar
camos y propiedades, a pesar de ser similares, semánticamente son distintas.
Un campo siempre debería ser privado, aunque C# nos deje hacerlo público,
los campos son variables que todos los métodos dentro de la clase son capaces
de acceder.
Para exponer el valor de un campo, debemos usar una propiedad, las propiedades
son como una forma de declarar 2 funciones con el mismo nombre, donde su significado
semántico es `get` y `set`, y la forma de llamar a estas funciones es mediante
la asignación u obtención de un dato a traves de la propiedad.
Por ejemplo, si nosotros hacemos:
Program.Propiedad = 10;
Asumiendo que `Propiedad` es una propiedad que admite el tipo `int`, estaríamos
llamando al método `set` dentro de la propiedad.
Pero si nosotros, en cambio, usamos el valor de la propiedad, ya se para
imprimirlo en la consola, esta va a llamar al método `get` y nos devolverá
lo que este retorne.
Console.WriteLine(Program.Propiedad);
Entonces, para declarar un campo, por ejemplo, la edad de una persona,
hacemos lo siguiente:
*/
// Los campos pueden hacerse de solo lectura con la palabra clave
// `readonly`, pero en este caso la edad es algo que cambia.
// El porqué el nombre es `_edad` lo hablaremos más tarde.
private int _edad = 18;
// Para demostrar el uso del `readonly` podemos tomar como
// ejemplo, el nombre, es algo que normalmente no cambia.
// Los campos `readonly` deben tener un valor, ya que no
// se pueden asignar después, como su nombre indica, son de
// solo lectura.
private readonly string _nombre = "Pepito Iglesias Fuentes";
/*
Si nosotros queremos exponer el valor de estos campos,
podemos usar propiedades, hay varias formas de declarar propiedades
y cada una de estas sirve para algo distinto.
Las propiedades pueden ser `auto` propiedades, eso significa que
estas no exponen ningun otro valor, simplemente almacenan lo que exponen,
muchas veces, el hecho de que puedas declarar una `auto` propiedad,
le quita la utilidad a los campos, pero siempre debes usar un campo
si es que no vas a exponer de ninguna forma el valor.
Para declarar una auto propiedad, la sintaxis es la siguiente
*/
public long Identificador { get; set; }
/*
Una `auto` propiedad puede tener `get` y `set` o solo `get`,
una `auto` propiedad con solo `get` se conoce normalmente como
una `Get only auto property` o la traducción al Español `auto propiedad
de solo obtención`.
Las propiedades `get only` han de tener un valor por defecto,
y las propiedades con `get` y `set` pueden tenerlo,
pero este no es requerido.
Esto pasa porque si una propiedad `get only` no tuviese un valor
por defecto, esta ya no podria asignarse, y tampoco tendria sentido.
Para asignar un valor, haremos lo siguiente:
*/
public double Altura { get; } = 1.79; // cm
/*
Si nosotros no ponemos un valor aquí, este se debe asignar
desde el constructor, de eso hablaremos cuando hablemos más a
fondo sobre los tipos y clases.
Si la propiedad fuese estática, como no habría forma de construirla
esta si requeriria tener un valor.
Para las propiedades `get only` hay otra forma de declararlas,
como propiedades computadas.
Al contrario que las `auto` propiedades normales, reevalúan la
expresión al acceder, mientras las `auto` propiedades normales
solo lo guardan.
Por ejemplo, si nosotros referenciamos una expresión computada
y esta llama a una funcion, por ejemplo la que declaramos antes
`Suma`, esta funcion se va a ejecutar tantas veces como nosotros
referenciemos esa propiedad.
Para declarar una propiedad computada haríamos lo siguiente.
*/
public bool EsMayorDeEdad => _edad >= 18;
/*
El caso de uso para una propiedad computada es cuando
queramos reevaluar un valor cambiante, por ejemplo la
longitud de una lista, o si una persona es menor de edad,
ya que la edad cambia y la longitud de una lista también
puede hacerlo cuando añadimos elementos a esta.
*/
/*
Luego existen las propiedades que exponen un valor existente, como
un campo, o que simplemente te permiten obtener el resultado
de una expresión.
Las propiedades normales que son `get only` y no son `auto` no se
usan tanto, ya que se priorizan las propiedades computadas para eso.
Pero por ejemplo, si deseamos en este caso hacer que se pueda obtener
y asignar la edad validando esos valores previamente, podemos hacer
una propiedad para ello.
*/
public int Edad
{
// También podria ser `get => _edad`,
// pero esto es para demostrar que es una funcion.
get
{
return _edad;
}
// En la funcion set, por defecto, hay una variable
// implícitamente declarada llamada `value`,
// esta contiene el operando derecho de la asignación,
// o sea, lo que el usuario ha asignado a `Edad` en esta caso.
set
{
// Si 0 es mayor que `value` o 100 es menor que value
if (0 > value || 100 < value)
throw new Exception("Edad invalida."); // tiramos un error, asi acabando el programa.
// Si la anterior condicion no se cumple, asignamos `_edad` como `value`
_edad = value;
}
}
/*
De esta forma, si nosotros hacemos `Program.Edad = 10`, se ejecutará el método `set` con el
valor de `value` siendo 10.
Ya que esencialmente, las propiedades son realmente un conjunto de métodos que tienen
un significado semántico más que otra cosa.
Cabe mencionar que a los métodos `get` y `set` también puedes ponerles
modificadores de acceso.
RETORNO RÁPIDO Y FUNCIONES LAMBDA
En distintos casos hemos visto el símbolo `=>`, este no significa igual o mayor que,
este más que nada representa una flecha, y se puede usar en declaraciones de métodos
y funciones para procesar una expresión compleja con parámetros y retornarlo de forma
más rapida y concisa.
Por ejemplo, podríamos crear un método que nos cree un array con números random,
con una sola línea usando LINQ
ver: https://learn.microsoft.com/en-us/dotnet/csharp/linq/.
*/
public static int[] CrearArrayRandom(int cantidad, int min, int max)
=> Enumerable.Repeat(0, cantidad).Select(_ => new Random().Next(min, max + 1)).ToArray();
/*
La anterior funcion, usa el método estático de `Enumerable` llamado `Repeat`,
este toma 2 parámetros, siendo este el valor, y cuantos elementos poner de ese mismo,
en este caso ponemos `cantidad` zeros. Luego lo encadenamos con el método `Select`
que lo que hace es obtener el valor, nos permito procesarlo y luego cambia
este por sea cual sea el resultado de la funcion lambda que hay como primer parámetro.
Luego convertimos eso a un array, y como usamos el `=>` ya directamente retorna el
resultado de la expresión.
Adentrándonos dentro del `lambda` mencionado anteriormente, una funcion lambda
es una funcion como expresión, una funcion lambda como expresión, retorna un `??`.
¿Pero qué es `??`? Por decirlo de alguna forma, la única manera que tenemos de
saber que retorna una funcion lambda y que parámetros toma, es explícitamente pidiéndolos,
al pedirlos se compara la compatibilidad del requerimiento contra la declaración de
la funcion lambda.
Más que nada en vez de decir "esta lambda es de tipo T", decimos "nos piden este requerimiento
¿Es nuestra lambda compatible con este?"
Para hacer ese requerimiento, lo que debemos hacer es declarar un delegado, ese
tiene información de nuestro requerimiento, por ejemplo, quiero una funcion lambda
a la que se le puedan pasar 2 números, y retorne un string,
entonces haríamos lo siguiente:
*/
public delegate string Requerimiento(int numero1, int numero2);
/*
Los delegados se pueden declarar en nivel superior, o sea, fuera de una clase,
y estos no pueden ser estáticos, ya que realmente son solo información,
implícitamente son estáticos, por lo que no podemos especificarlo explícitamente.
Luego, podemos pedir eso en una funcion o método.
*/
// Declaramos un método que pida una lambda de tipo `Requerimiento`
public static string HacerAlgoConRequerimiento(Requerimiento req)
{
// Creamos una variable de tipo `Random` que es una clase
// que nos permite generar valores random. I la inicializamos
// con una nueva instancia, como ya especificamos el tipo,
// C# entiende que con solo poner `new()` estamos haciendo
// `new Random()`.
Random random = new();
// Llamamos a la funcion que nos pasaron como parámetro con
// 2 números random, y sea cual sea el resultado lo retornamos.
return req(random.Next(0, 10), random.Next(0, 10));
}
/*
Luego, como anteriormente con el `Select`, podríamos llamarla de forma
similar, En el caso del `Select` como no nos importaba el contenido
del parámetro, poníamos `_`, para C# un `_` significa que no nos
importa lo que haya en el parámetro cuando requerimos declararlo.
Entonces, para llamar a la funcion, vamos a ignorar el segundo
parámetro y convertir a string el primero, para eso haríamos
HacerAlgoConRequerimiento((num1, _) => num1.ToString());
Y eso nos retornaria ese número, en este caso, lo que es el método
no tiene mucho sentido, pero es más una demostración de lo que
hacen los delegados.
EVENTOS
Dentro de una clase, podemos declarar eventos, esos son esencialmente
delegados a los que te puedes subscribir.
Para declarar un evento, este como tipo ha de tener un delegado si o si,
y puede ser estático o no, por ejemplo, si tuviésemos un bot de discord,
para saber cuando se mandó un mensaje a un canal, tendríamos un evento como
`MensajeRecibido`, vamos a declararlo.
*/
// Primero declaramos un delegado para el evento.
public delegate void DelegadoDeMensajeRecibido(string mensaje);
// Declaramos el evento, en este caso de forma no estática, ya que formaria
// parte de una conexión que debemos instanciar.
// Este por defecto lo subscribimos con un `delegate { }`, que eso
// es crear una funcion lambda vacía sin parámetros, si no lo hacemos asi,
// a la hora de invocar el evento, nos dara un error diciendo que no hay
// nada subscrito al evento.
public event DelegadoDeMensajeRecibido CuandoSeRecibaUnMensaje = delegate { };
/*
Para subscribirse y de subscribirse del evento fuera de la clase,
lo que haremos será usar los operadores `+=` y `-=`, en este caso
tienen una funcionalidad especial, ya que son parte de un evento,
en este caso nos permite subscribir y de subscribir la referencia de
un método o funcion, si usamos una lambda, este no podria ser de subscrito,
porque esos se generan ahí y no hay forma de referenciarlos.
Cliente.CuandoSeRecibaUnMensaje += mensaje => Console.WriteLine(mensaje);
Aquí lo que estamos haciendo sería que cuando se reciba un mensaje,
la funcion lambda recibiría el mensaje y lo imprimiria a la consola.
Y dentro de la clase, si estuviésemos en la declaración de cliente,
lo que podríamos invocar ese evento, y todos los métodos subscritos
se ejecutarían.
Para eso solo lo llamamos como una funcion normal.
CuandoSeRecibaUnMensaje("ejemplo");
En ese caso, el mensaje que recibirían los subscriptores, sería "ejemplo".
Fuera de la declaración no se puede llamar al evento como funcion.
*/
} // VAMOS A SALIR DE LA CLASE, ESTO SERIA TOP LEVEL
// AQUÍ FUERA SOLO PODEMOS DECLARAR CLASES, STRUCTS, ENUMS, DELEGADOS, INTERFACES Y RECORDS.
/*
Hemos hablado demasiado de lo que puede ir dentro de una clase y lo que no,
pero no hemos hablado mucho de clases, asi que
CLASES Y STRUCTS
En C# como en muchos otros lenguajes hay 2 formas de mover y referenciar objetos,
uno es por copia y el otro es por referencia, ¿Pero qué significa esto?
Las clases son un tipo de objeto que funciona por referencia, básicamente,
si pasamos una instancia de una clase por una funcion como parámetro,
cuando la funcion modifique una propiedad, esta se modificara en la instancia
principal, por ejemplo
Persona persona = new(); // creo una instancia de una persona
HacerAlgoConUnaPersona(persona); // llamo a una funcion pasándole el parámetro.
Y si dentro de la declaración de la funcion, asumiendo que la clase Persona
tiene una propiedad llamada `Nombre` hacemos lo siguiente:
persona.Nombre = "Pepe"; // esto seria dentro de la funcion
Entonces, si fuera de la funcion yo trato de leer el nombre, veremos que este
ha sido modificado, ya que una clase solo contiene referencias de objetos
en memoria y no los objetos como tal.
Pero si, en cambio, pasamos un struct, ahora asumiendo que Persona es un struct
si yo dentro de la funcion hago lo anterior, esto no se verá reflejado en el
objeto fuera de esta funcion, porque al pasarla por un parámetro, esta se copia
completamente, copiando los objetos que tiene en su interior.
Cabe mencionar que si tenemos un struct llamado casa, con una lista de personas
como lo que asumimos que es una auto propiedad que guarda una lista, está
al ser una clase, realmente lo único que ocurrirá es que referenciáramos
a la misma memoria que el objeto pasado, ya que "copiar" realmente hace un
shallow copy, y no un deep copy
ver: https://stackoverflow.com/questions/18066429/shallow-copy-or-deep-copy.
Entonces, como son 2 tipos de estructuras distintas las gestionamos de
forma un poco distinta, por ejemplo, el struct al ser una copia, tiene la
palabra clave `readonly` como parte de su declaración, cosa que nos permite
no ser capaces de modificar los objetos que almacene este mismo (pero si los
objetos que almacenen los objetos que almacene el struct).
Ya que hablamos de referencias, volviendo un poco al tema de variables
nosotros podemos usar la palabra clave `ref` que en corto significa `referencia`.
Por ejemplo, si nosotros tenemos una variable entera de la siguiente forma
int variable = 10;
Nosotros podemos crear una referencia a esta creando una variable del mismo
tipo, pero el tipo siendo prefijado con la palabra clave, y el valor también.
ref int referenciaAVariable = ref variable;
Pero, Porque ponemos 2 veces la palabra `ref`, es sencillo, vamos a dividirlo en
2 partes. Cuando nosotros creamos una variable como `ref` esta solo puede almacenar
referencias, entonces, cuando le asignamos `ref variable` lo que estaríamos
haciendo es pasarle una referencia a esa variable, y no el valor en sí.
A diferencia de lenguajes como C++, aquí para obtener la referencia
siempre usamos la palabra clave `ref`, si no, la referencia al nombre nos devolverá
el valor.
En métodos y funciones también podemos definir parámetros por referencia prefijando
el tipo con `ref`.
Si nosotros modificamos una variable con referencia, por ejemplo haciendo
referenciaAVariable = 10;
entonces al referenciar variable, nosotros obtenemos el valor cambiado.
Console.WriteLine(variable); // 10
Ahora que entendemos la principal diferencia entre las 2, vamos a explicar su
declaración una por una.
La sintaxis de declaración de una clase es la siguiente
(public|internal|file) ((static|<none>)|(abstract|<none>)|(sealed|<none>)|(partial|<none>))
class {nombre} (: (<parent type>|<none>), ...(<implemented interfaces>|<none>)) ({cuerpo}|;)
Esta explicación de sintaxis puede ser un poco confusa, pero vamos a explicar todas las
palabras claves que hay aquí y también que significa implementar una interfaz y un tipo padre.
Vamos a aplicar un ejemplo práctico en una clase.
*/
// Una clase de persona, no estática, podemos crear instancias.
// Definimos una clase partial, posteriormente hablaremos de por qué.
public partial class Person
{
public string Name { get; init; } // El nombre es público, pero solo se puede iniciar, no asignar.
private readonly DateTime _birth; // Creamos un campo, que es totalmente privado que sería cuando nació.
// Tenemos una propiedad que sería el cálculo de la edad de la persona (propiedad computada).
public int Age => DateTime.Now.Year - _birth.Year;
// Esto sería una auto propiedad para la vida en un videojuego.
public float Health { get; private set; } // No queremos exponer el asignar vida, ya que no tendria sentido.
// Esto es un constructor, a la hora de hacer `new` define los parámetros que se le pasan.
// Tiene el parámetro de nombre, y el parámetro opcional de birth, que sería cuando nació.
public Person(string name, DateTime? birth = null)
{
Name = name; // Le asignamos el nombre a la instancia, consumiendo asi el init.
_birth = birth ?? DateTime.Now; // Le asignamos el nacimiento con un fallback, `tiempo actual si birth es null`
Health = 100; // La vida del jugador/persona empieza por 100.
}
// Vamos a hacer un `overload` para tener otro constructor, de esta forma
// podemos invocar la misma clase con 2 formatos de parámetro distintos.
public Person(string name, int age)
{
Name = name;
_birth = DateTime.Now.Subtract(TimeSpan.FromDays(365 * age));
Health = 100;
}
/*
Los constructores también pueden ser declarados arriba de la clase,
esto se conoce como un constructor primario, Ese no nos permitiría
poder hacer cosas dentro del constructor como si de un método se tratase.
Pero de otra forma podríamos acceder a los parámetros a traves
de toda la clase
ver: https://learn.microsoft.com/es-es/dotnet/csharp/language-reference/proposals/csharp-12.0/primary-constructors.
Más adelante veremos alguna implementación de un constructor primario.
*/
// Declaramos un método para encapsular que se le pueda quitar vida al jugador.
public void Damage(float health)
{
Health -= health; // Le decimos que quite la cantidad de vida asignada.
}
}
/*
La declaración de un struct es la misma, lo único que con la palabra clave
struct, sus pros y contras y la habilidad de hacerla `readonly`
ver: https://learn.microsoft.com/es-es/dotnet/standard/design-guidelines/choosing-between-class-and-struct.
El ejemplo anterior define una clase llamada `Person` que representa a una persona con sus
propiedades y campos más básicos, también tenemos la habilidad de construirla con el constructor,
si este no se define, se podría inicializar sin parámetros.
Si el constructor es privado, este no se podrá inicializar desde fuera, esto nos permite
encapsular la inicialización de nuestra clase.
Ahora que hablamos un poco de la definición de la clase con todos los ejemplos anteriores que
dimos sobre propiedades, campos, funciones y más... Vamos a hablar sobre interfaces.
INTERFACES Y ENUMS
Entonces, imagina el caso donde nosotros queramos que la persona sea capaz de tener un arma,
en el contexto de un videojuego eso sería válido, no entraremos en el tema de coordenadas,
ya que no tenemos un juego hecho ni nada, pero nos sirve como ejemplo.
De armas hay muchas y de muchos tipos, y claro, también de clases, hay muchas y de muchos tipos
como esas que no son armas ni tienen nada que ver con ello.
En este caso, usaríamos una interfaz, una interfaz es una promesa de que una clase
tendra una serie de propiedades y funciones, como por ejemplo la funcion Disparar,
La vida que toma por disparo y más información relacionada con un arma.
Entonces vamos a crear una interfaz que prometa que lo anterior mencionado esté
en una clase que la implemente.
*/
public interface IWeapon // Esta interfaz será implementada con `:` por cualquier tipo de arma.
{
public float DamagePerShot { get; } // Cuanto daño hace por disparo.
public int ShotsPerAttack { get; } // Cuantos disparos hace por cada ataque.
public void Attack(Person person); // La acción de atacar.
}
/*
Ahora que tenemos una interfaz, podemos crear una clase de arma.
Que la clase tenga que implementar lo que hay en la interfaz no significa que no pueda
tener más campos, en este caso como un arma puede ser una espada, esta no se tendria que
recargar, podríamos crear una interfaz que implemente esa interfaz para añadir la
obligación de poner la accion de recargar, pero no vamos a hacer eso ahora.
*/
public class Ak47 : IWeapon
{
public float DamagePerShot => 2; // 2.5 de vida por disparo.
public int ShotsPerAttack => 2; // 2 disparos por ataque.
public void Attack(Person person)
{
// Llamamos a la funcion `Damage` de `Person` con el daño que le haga el disparo
person.Damage(DamagePerShot * ShotsPerAttack);
}
}
/*
Muy bien, ahora tenemos un arma, pero le hemos de dar a la persona la opción de
obtenerla.
Para eso vamos a usar la parcialidad anterior para definir que pueda obtener esa arma.
Cuando hacemos una clase parcial, esta nos permite extender la implantación,
entonces, si en un lado ponemos un método, y en el otro una propiedad, la
implantación final va a ser una sola.
*/
// En esta parte de la implementación vamos a definir un método para obtener esa arma,
// asi como una propiedad para el arma que obtuvo.
// Vamos a hacer lógica para que esta pueda tener una arma secundaria.
public partial class Person
{
// Las armas equipadas pueden ser null porque el jugador puede no tener arma equipada.
private IWeapon? _primaryWeapon; // La arma primaria.
private IWeapon? _secondaryWeapon; // La arma secundaria.
}
/*
En esta parte de la clase, vemos como tenemos la capacidad de tener 2 armas,
entonces, debemos tener alguna manera de saber cuál es el arma que tenemos
equipada para saber qué tirar, que remplazar...
Para hacer eso, vamos a usar enums.
Un enum más que nada es la capacidad de nombrar y dar un sentido a los números,
si tenemos una serie de valores prefijados, o queremos una forma limpia y
pequeña de asignar nombres a cosas que se puedan enumerar, podemos usarlos.
Por ejemplo, podemos crear un enum que nos permita saber que arma tenemos equipada,
para ello vamos a declararlo.
*/
[Flags] // Especificamos que el enum son banderas, posteriormente explicadas.
public enum EquippedWeapon
{
Primary,
Secondary,
}
/*
Ahora tenemos un enum con 2 valores, pero para qué sirve y que significa,
Básicamente con esto ahora podemos simplemente decir "¿Que arma tengo equipada,
la primaria o secundaria?"
Los valores de los enum por defecto son 0, 1, 2, 3 y la cantidad que tengamos,
pero si queremos asignarles un valor específico, también podemos hacerlo,
para ello usaremos el operador de asignación, en este caso
Primary = 0,
Ahora vamos a completar las definiciones faltantes para la gestion de armas
en la clase persona.
*/
public partial class Person
{
private EquippedWeapon _equippedWeapon;
// Creamos un método para obtener la referencia del arma
// que esté equipada, basándonos en el valor de _equippedWeapon
private ref IWeapon? GetWeapon()
{
// usamos el operador ternario
return ref _equippedWeapon == EquippedWeapon.Primary // evalúa esta condicion
? ref _primaryWeapon // devuelve eso si el resultado de la evaluación es verdadero
: ref _secondaryWeapon; // o este si es falso
}
// Definimos un método para tirar el arma, que realmente es
// definir el arma actual como null, como nosotros estamos
// obteniendo una referencia, por eso podemos asignar la expresión,
// ya que lo que nos está retornando el método `GetWeapon` es
// memoria assignable válida.
public void DropWeapon()
=> GetWeapon() = null;
// Esta funcion asignará un nuevo arma a la referencia del
// arma actual que esté equipada.
public void GrabWeapon(IWeapon weapon)
=> GetWeapon() = weapon;
// Esta funcion actuaria como un swap de arma, como si moviésemos la
// ruedita del ratón para cambiar o si pulsáramos el triángulo...
public void SwapWeapon()
{
// Esto hace una operación de bits para cambiar el valor de EquippedWeapon dependiendo
// de su valor, básicamente invirtiéndolo
// ver: https://www.geeksforgeeks.org/swap-two-numbers-without-using-temporary-variable/
_equippedWeapon = _equippedWeapon ^ EquippedWeapon.Primary ^ EquippedWeapon.Secondary;
// También se podria hacer con un operador ternario
// _equippedWeapon = _equippedWeapon == EquippedWeapon.Primary
// ? EquippedWeapon.Secondary
// : EquippedWeapon.Primary;
}
// De nuevo, obtenemos la referencia del arma actual, con `?.` comprobamos
// que no es null y si no lo es, llamamos al método `Attack` en la instancia.
// Entonces, si el arma actual es nula, no hará nada, si no lo es, atacará la persona.
public void Attack(Person victim)
=> GetWeapon()?.Attack(victim);
}
/*
Vimos como podemos hacer la lógica para que una persona pueda tener armas,
atacar a otras personas...
Pero vamos a ver que son los flags y como realmente se comportan los números en un enum.
Un enum realmente es una forma de crear constantes, asi haciendo nuestro código más
legible y menos propenso a errores.
Por ejemplo, si nosotros tenemos lo siguiente
const int Administrador = 0;
const int ModificarMensajes = 1;
const int EliminarMensajes = 2;
const int EliminarUsuarios = 3;
Esto lo podríamos convertir a un flagged enum con
valores multiples de 2.
*/
[Flags]
public enum Permisos
{
// IMPORTANTE: PARA LOS FLAGS LOS NÚMEROS SIEMPRE DEBEN SER MULTIPLES DE 2
Ninguno = 0,
Administrador = 1 << 1,
ModificarMensajes = 1 << 2,
EliminarMensajes = 1 << 3,
EliminarUsuarios = 1 << 4
}
/*
Entonces, con este enum nosotros podemos tener un solo número int que nos permita
almacenar varios valores.
Antes de nada debemos saber que un ordenador funciona con bits, entonces, los números
se deben convertir a bits antes de hacer las operaciones binarias de cabeza.
Vamos a representar este número con un campo dentro de `Player`.
*/
public partial class Person
{
// Aquí vamos a guardar los permisos de la instancia.
private Permisos _permisos = Permisos.Ninguno;
// Definimos un método para mirar si una o varios flags existen en
// los permisos de la instancia de `Person`
public bool HasPermission(Permisos flags)
// Con el operador AND podemos ver si _permisos & flags hace match.
=> (_permisos & flags) != 0;
/*
En más profundidad, el cómo funciona el AND es separando los bits y comprobando
si estos hacen match.
Con el caso de administrador, como esta es 00000010, entonces comprobamos si
hace match en algún sitio de todos los bits que hay en _permisos
Por ejemplo, si tenemos (ModificarMensajes | EliminarUsuarios) esto en binario
se representa como 00001100.
Lo que haría el AND en este caso es mirar donde hace match, y nos devolverá
el número que quede de eso, en este caso 0, porque ninguno de los flags que hay
en (Administrador) hace match a (ModificarMensajes | EliminarUsuarios)
00000010
00001100
AND 00000000
El and hace lo siguiente
1 & 1 == 1
0 & 1 == 0
1 & 0 == 0
0 & 0 == 0
Por cada número.
Entonces con el resultado que nos quedamos es con, (si la comparación no es 0),
y esto resulta en un valor booleano.
Ahora vamos a declarar un método que nos permita añadir flags a los permisos
del jugador, para eso vamos a usar un OR.
*/
public void AddPermission(Permisos permisos)
// Asignamos el OR de _permisos y permisos a _permisos.
=> _permisos |= permisos;
/*
El OR en principio hace lo mismo que el AND, pero el resultado es distinto
1 | 1 == 1
0 | 1 == 1
1 | 0 == 1
0 | 0 == 0
Entonces, si tenemos el permiso ModificarMensajes, que es el 4 o sea 00000100,
luego el permiso que queremos añadir el permiso EliminarMensajes que es el 8
o sea 00001000, con una comparación OR, lo que hacemos es eso:
00000100
00001000
OR 00001100
Entonces tendríamos el flag añadido.
Luego, para quitar un permiso, vamos a declarar otro método llamado `RemovePermission`.
*/
public void RemovePermission(Permisos permisos)
=> _permisos &= ~permisos;
/*
El cómo funcion eso es de la siguiente manera, primero vamos a descomponer
la expresión en partes, primero tenemos él (~permisos)
El operador ~ en C# invierte todos los bits de un número, entonces una vez
invertidos, si tenemos los permisos de ModificarMensajes y EliminarMensajes
o sea (ModificarMensajes | EliminarMensajes), eso es en binario 00001100,
luego imagina que el valor que queremos quitar es el de ModificarMensajes
entonces, este es el equivalente a 00000100, si nosotros invertimos eso es
11111011.
Entonces, si hacemos un AND entre los flags y el flag invertido que queremos quitar
nos queda lo siguiente:
11110111
00001100
AND 00000100
Ahora ese resultado AND, lo asignamos a _permisos, de esta forma, nos quedamos
con los permisos correspondientes.
*/
}
/*
Bien, ahora que entendemos un poco más sobre números, interfaces y enums, nos podemos
adentrar más en como funciona el modificador `static`
Como vimos anteriormente, el modificador `partial` sirve para partir la declaración
de una clase en varias partes, pero ahora vamos a ver otros modificadores como `static`.
En el caso de static, vimos un poco que es para definir si se pueden hacer instancias
de una clase o no, pero no profundizamos mucho, entonces, en general sabemos que hacer una
instancia en este caso de `Person` se hace de la siguiente forma:
Person person1 = new("Juan"); // los parámetros como los definimos en el constructor
Person person2 = new("Pepe");
Si la clase `Person` fuese `static`, nosotros no podríamos crear instancias de esta,
tampoco declarar constructores ni tener miembros que no sean `static`, al contrario
de una clase que no sea `static`, esta sí que puede contener miembros que sean `static`.
La diferencia entre `static` y no `static` es que para los miembros `static` no necesitas
crear una instancia para acceder al miembro, tomando como ejemplo el método `ReadLine`,
si este no fuese `static` nosotros tendríamos que crear una nueva instancia de `Console`
para poder llamarlo.
new Console().ReadLine();
Pero eso no tiene mucho sentido, además que la clase `Console` es `static` y no se pueden
crear instancias de ella.
Cuando nosotros hacemos un método que no es `static` dentro de su declaración tenemos
acceso a la palabra clave `this`, que en resumidas cuentas es un puntero a la propia instancia.
Como ejemplo de `this` tomemos que tenemos la instancia
Person person1 = new("Juan");
Si nosotros dentro de un método dentro de la clase accedemos a `this.Nombre` tendríamos
"Juan", ya que `this` en este caso sería como hacer `person1.Nombre`, lo único que desde
dentro, donde no tenemos acceso al nombre `person1`.
Como vimos anteriormente, también podríamos acceder a `Nombre` sin usar `this`, si este
es el único miembro con el mismo nombre en este caso `Nombre`, C# entiende que
estamos accediendo al miembro de la instancia sin especificarlo explícitamente, y el
código generado lo incluye automáticamente.
Para evitar conflictos de nombres usamos algo llamado coding conventions, en este caso
C# tiene unas de hechas que nos permiten hacer que nuestro código sea menos confuso,
en casos como los de unity que también usa C#, este tiene otras coding conventions.
Si queremos hacer que nuestro código sea entendible para el que sea que lo vaya a leer,
seguir las conventions de la plataforma donde estemos programando es muy importante
ver: https://learn.microsoft.com/es-es/dotnet/csharp/fundamentals/coding-style/coding-conventions
ver: https://unity.com/how-to/naming-and-code-style-tips-c-scripting-unity.
En cuanto a clases estáticas, también tenemos la inicialización de los miembros,
por ejemplo, un miembro no se inicializa hasta que lo referenciamos por primera vez.
Las clases estáticas también tienen constructores estáticos, que se ejecutan cuando
accedes a un miembro de la clase desde fuera de cualquier manera.
Sabiendo que todas las clases sean estáticas o no pueden contener miembros estáticos
también podemos declarar un constructor estático en cualquiera de estas.
Para declarar un constructor estático se hace de la siguiente forma
*/
public static class StaticTest
{
// Los constructores estáticos no pueden tener parámetros.
static StaticTest()
{
// Esto es un contexto estático, significando
// que no podemos acceder a miembros no estáticos
// desde aquí.
// Este método se ejecutará cuando intentemos referenciar
// a un miembro estático de esta clase.
// No hay forma de poderlo referenciar de otra forma.
}
}
/*
Ahora que tenemos un poco más claro el cómo funciona la palabra clave static, vamos a
adentrarnos en instancias de nuevo y ver como funciona la
HERENCIA DE CLASES Y CONVERSION DE TIPOS
:)
Con las interfaces, somos capaces de implementar varias de esas en una clase,
y posteriormente guardar una instancia de esa clase en una variable del tipo de la interfaz,
a diferencia de una clase, de las interfaces no podemos crear instancias, esas solo proveen
información sobre lo que contiene un tipo.
Algo similar pasa con las clases, si la clase no es sellada puedes extenderla similarmente como
lo harías con una interfaz, la diferencia es que con las interfaces, puedes implementar muchas,
pero de clases solo puedes extender una.
A este concepto se le conoce como "multiple-inheritance" o "herencia-multiple", y C# no la soporta.
Cuando tú extiendes una clase obtienes acceso a todos los campos, propiedades y métodos que
tengan el modificador de acceso `public`, `protected` e `internal`, literalmente heredándolos.
No puedes aplicar herencia a clases estáticas, ya que están hechas para guardar
miembros y valores de forma estática, al estos poderse acceder de forma directa
sin una instancia imposibilita el hecho de que estas puedan heredar, asi también
quitándole el sentido.
Todos los miembros que sean estáticos dentro de una clase no estática, van a ser
visibles para todos para todas las clases que hereden de esa, pero de forma
estática.
Vamos a poner un ejemplo de como sería heredar en estructuras del mundo real, para eso
vamos a crear una clase de `Vehicle` y vamos a hacer que `Car` extienda sus propiedades,
al igual que `Bicycle`, ya que los 2 son vehículos, pero se comportan de distinta forma
*/
public class Vehicle
{
protected float MaxSpeed { get; set; }
public int CoordinateX { get; private set; } // asignamos las coordenadas
public int CoordinateY { get; private set; } // por defecto en 0.
// hacemos el constructor protegido, haciendo que no se puedan crear instancias de esta clase.
protected Vehicle(float maxSpeed)
{
MaxSpeed = maxSpeed; // asignamos speed en el constructor usando el `this` implícito.
}
public void Move(int x, int y)
{
while (CoordinateX != x || CoordinateY != y)
{
int deltaX = x - CoordinateX; // calculamos la distancia restante
int deltaY = y - CoordinateY;
CoordinateX += Math.Sign(deltaX) * (int)Math.Min(Math.Abs(deltaX), MaxSpeed); // movemos el vehiculo
CoordinateY += Math.Sign(deltaY) * (int)Math.Min(Math.Abs(deltaY), MaxSpeed); // a un máximo de la velocidad.
}
}
}
/*
Ahora que tenemos una clase base, esta la podríamos heredar, haciendo un coche por ejemplo.
*/
public class Car : Vehicle
{
/*
Como esto es un coche, y la mayoría tienen marchas, vamos a implementar eso,
para eso creamos una auto propiedad get que nos permita obtener a la
marcha a la que vamos, eso por si requerimos hacer un contador de marchas.
Pero que el set este encapsulado por 2 métodos `UpShift` y `DownShift`,
asumiendo que el cambio de marchas es secuencial, como en la
mayoría de videojuegos.
*/
public int CurrentGear { get; private set; }
private float BaseSpeed { get; }
/*
Cuando heredamos una clase que tiene un constructor, obligatoriamente
hemos de definir un constructor que rellene el constructor de la clase base.
Puede ser a traves de un parámetro o una expresión estática (que no tenga nada
que ver con la instancia).
Los constructores siempre han de tener cuerpo, si no lo utilizas déjalo vacío.
De constructores podemos declarar más de uno por clase, a pesar de que siempre
deben tener algo que los diferencie, en este caso parámetros.
*/
public Car() : base(120)
{
BaseSpeed = 23; // kilómetros por hora de base
}
public void UpShift()
{
if (CurrentGear < 7) // Si la marcha actual es menor que 7
CurrentGear++; // la aumentamos por 1
// hacemos que la velocidad maxima sea la base multiplicada por la marcha.
MaxSpeed = BaseSpeed * CurrentGear;
/*
Lo anterior podria ser una propiedad computada,
eso lo veremos luego.
*/
}
public void DownShift()
{
if (CurrentGear > 0) // Si la marcha actual es mayor que 0
CurrentGear--; // la disminuimos por 1
MaxSpeed = BaseSpeed * CurrentGear; // hacemos lo mismo que antes.
}
}
/*
Asi también podríamos crear clases como avión, la cual no va con marchas,
sino con revoluciones. Y tiene otros controles como los FLAPS...
O una clase de furgoneta, que realmente es un coche más grande y esta
podria extender directamente de Car si es que asi lo adaptamos.
Asi creando un árbol gigante de clases que heredan a otras clases.
Con las clases que hemos creado, podemos ver que existen métodos como `Equals`
o `ToString` aunque nosotros no los hayamos creado.
Eso es porque todas las clases extienden de `object`, este siendo el padre
de todas las clases.
Posteriormente, veremos el cómo podemos sobreescribir el comportamiento de estas.
En el caso de los structs es un poco distinto, en vez de extender de `object`
extienden de otra clase que se llama `ValueType`, que es la que define el cómo
se comporta un struct, pero a la vez `ValueType` extiende de `object`, asi que
las variaciones existen, pero la base acaba siendo `object` de todas formas.
Lo anterior también pasa con los enum y varias otras clases con el mismo comportamiento
como el `int` o el `float`
ver: https://learn.microsoft.com/es-es/dotnet/api/system.valuetype?view=net-8.0.
Ahora vamos a hablar un poco sobre la conversion de tipos aún más en profundidad,
antes vimos que podemos convertir un int a un float haciendo `(float)` a un valor entero.
Hay distintos casos en el porqué podemos o no convertir objetos de un tipo a otro,
y eso es porque el lenguaje, aparte de funcionar como nosotros esperamos que lo haga
también tiene cosas especiales, como por ejemplo el hecho de convertir de int a float,
estas 2 clases no tienen nada que ver en el árbol de herencia, pero está definido
en el lenguaje que podamos hacerlo, sin más.
Hay que pensar que esto es un lenguaje de programación y realmente lo que pase aquí dentro
está especificado en estándares, y hay cosas que suceden sin más y porque están especificadas allí.
Para el ejemplo de int, float y double y el porqué podemos convertir esos valores sin
qué estos hereden unos de otros
ver: https://learn.microsoft.com/es-es/dotnet/csharp/language-reference/builtin-types/numeric-conversions.
Luego existe la conversion que podemos hacer nosotros y que sigue unas reglas más claras.
Para la conversion de tipos hay 2 formas, llamadas de maneras distintas, estas son `Parsing` y
`Casting`, vamos a primeramente hablar del `Casting`.
El casting es como lo hemos visto antes, poner paréntesis, el tipo delante del valor o expresión
a convertir, y este nos devolverá el tipo convertido, por defecto nosotros podemos hacer casting
a la clase padre del árbol de herencia, y viceversa, eso sí, también tenemos la posibilidad
de convertir de otra clase, a la clase padre a otra clase que no tenga nada que ver con la
que creamos en un principio.
Cuando nosotros hacemos un cast al tipo padre, perdemos el acceso a la información, a pesar
de que esta está ahí y C# sabe el tipo inicial del valor.
Si por ejemplo nosotros pedimos una variable de la clase padre en un método como argumento,
vamos a ser capaz de pasar todos los tipos hijos de esa clase, si queremos hacer casting
para acceder a la información del tipo completa, como vimos anteriormente podemos usar
la palabra clave `as` para convertirlo, asi si esta nos devuelve null sabemos que
no es el tipo correcto y podemos gestionarlo correctamente.
También sí obtenemos un valor del tipo padre, como vehículo y queremos hacer algo
dependiendo del tipo de vehículo que sea, en C# hay un concepto llamado pattern matching.
El pattern matching tiene la siguiente sintaxis
if (<object> is <Type> <casted>)
{
}
Con esto podemos mirar si un objeto es de ese tipo, y si lo es tener acceso a `casted` que en este
caso es una variable que nosotros podemos nombrar como sea, está como valor
tendra `object` pero convertido a `Type`.
En el caso de la gestion de vehículos, podemos gestionar eso de la siguiente forma
public void HandleService(ServiceVehicle vehicle)
{
if (vehicle is Taxi taxi)
{
taxi.GrabPassengers();
taxi.DeliverPassengers();
}
if (vehicle is Van van)
{
van.GrabCargo();
van.DeliverCargo();
}
if (vehicle is PizzaBike bike)
{
bike.GrabPizza();
bike.DeliverPizza();
}
}
Si los 2 tipos no tienen nada que ver, tu IDE debería darte un warning diciéndote que
esos tipos no tienen nada que ver.
El pattern matching incluye más cosas, puedes verlo en la documentación de Microsoft Learn
ver: https://learn.microsoft.com/es-es/dotnet/csharp/fundamentals/functional/pattern-matching
El `Cast` se considera un operador unario. Si tenemos 2 tipos que no tienen nada que ver
en el árbol de herencia, podemos declarar este operador para convertir de un tipo a otro,
En este caso, previamente hemos declarado una clase de Coche y una clase de persona,
como en transformers, podemos convertir un vehículo a persona, y viceversa, a pesar
de que eso en un ejemplo de mundo real no tendria mucho sentido, pero probablemente
si en un videojuego.
Para declarar un operador, previamente debemos saber la definición de explícito e implícito.
Según la RAE:
Explicito: Qué expresa clara y determinadamente una cosa.
Implícito: Incluido en otra cosa sin que esta lo exprese.
Traducido a C#, esto significaria
Explicito: Tenemos que usar el operador `(Tipo)` para convertir el tipo si o sí.
Implícito: El tipo se convierte automáticamente si el contexto lo requiere.
Entonces, vamos a crear un transformer :)
*/
public partial class Person
{
// Los operadores siempre son estáticos, el parámetro es el tipo que pretendemos convertir.
// Nosotros usaremos `_` para decirle a nuestro IDE que no vamos a usar el parámetro.
public static explicit operator Car(Person _)
// Como realmente estos 2 no tienen nada que ver, simplemente retornaremos un nuevo coche,
// pero en el caso de que tuvieran cosas que intercambiar, se podria hacer especificando
// un cuerpo como si de una funcion se tratase.
=> new();
}
/*
Ahora si nosotros tenemos una instancia de `Person`, podemos convertirla a un `Car`
y C# no se va a quejar.
Car transformed = (Car)new Person("Juan");
A parte del casting, tenemos otro patron de conversion que podemos usar, como en el caso de
`int.Parse(string)`, este nos permite convertir un string a un `int` con el método parse.
Esto sería una forma semánticamente más correcta de convertir strings a tipos de datos, más como
una deserialización de estos
ver: https://es.wikipedia.org/wiki/Serializaci%C3%B3n.
La forma normal de implementar los métodos a nuestra clase es implementando la interfaz `IParsable<TTarget>`
a nuestra clase.
Vamos a implementar una forma de convertir la string "nombre, edad" a una instancia de persona.
*/
// Implementamos `IParsable<Person>` en `Person?`, siendo `Person?` el tipo objetivo.
// El símbolo de pregunta es porque `Person` en este caso puede ser null si la conversion falla.
public partial class Person : IParsable<Person?>
{
// Parse debe hacer la conversion y tirar un error si esta falla.
// Como vamos a tirar un error, aquí no hará falta retornar null si falla
// ya que si falla no vamos a retornar.
public static Person Parse(string s, IFormatProvider? provider = null)
{
// Vamos a validar el string y separarlo en partes, nosotros queremos un string con
// "nombre, edad"
// Esto convierte la string a un array de la string separada por el carácter ',' en este caso.
string[] split = s.Split(',');
if (split.Length != 2) // Si la longitud no és 2
throw new Exception("El string debe contener máximo 1 coma."); // Tiramos un error.
// Retornamos una nueva instancia de Person.
return new Person(
split[0].Trim(), // El nombre con los espacios del principio y el final quitados.
DateTime.Parse(split[1].Trim(), provider) // Usamos el método estático `Parse` de `DateTime`.
);
}
// Try parse debe tratar de convertir, pero no tirar un error si la conversion falla,
// para eso vamos a hacer uso de un `try, catch`.
public static bool TryParse(string? s, IFormatProvider? provider, out Person? result)
{
try
{
if (s is null) // miramos que s no sea null con pattern matching.
throw new NullReferenceException(); // eso lo pasaríamos al catch.
// asignamos result como el resultado, ya que este es un parámetro out, para más información,
// ver: https://learn.microsoft.com/es-es/dotnet/csharp/language-reference/keywords/method-parameters#out-parameter-modifier
result = Parse(s, provider);
// retornamos true como que la conversion fue correcta.
return true;
}
catch
{
// Aquí cualquier tipo de error que se tire en la anterior operación va a hacer
// que result sea null y retornemos false.
result = null;
return false;
}
// Como en los 2 caminos posibles hay un return, no hace falta especificarlo abajo.
}
}
/*
De esta forma tendríamos habilitado en nuestra clase el hecho de poder `Parserala`.
Si hacemos `Person.Parse("joaquin,50")` tendríamos una instancia `Person`
basado en los datos que había en el string, en un caso de uso real, es deserialización de datos,
si por ejemplo queremos abrir un archivo donde hay muchos nombres y edades separadas por `, `,
podríamos usar un `Aggregate` de LINQ para almacenar todos esos en un `Array`.
Para acceder al sistema de archivos tenemos la API de `File` que C# provee, esta tiene
métodos estáticos como `ReadLines` y más, para eso usaremos el método anteriormente mencionado.
Person[] persons = File.ReadLines("./persons-list.txt").Select(p => Person.Parse(p)).ToArray();
LINQ puede ser un poco confuso para empezar, es recomendable leer la guía de LINQ de microsoft para
evitar confusiones
ver: https://learn.microsoft.com/es-es/dotnet/csharp/linq/get-started/introduction-to-linq-queries.
En conclusion, lo que hace `Select` es convertir datos de un enumerable.
Más adelante veremos apis útiles que tiene el lenguaje como por instancia LINQ, pero por ahora veremos
un poco más sobre semanticidad en clases y diseño de programa,
Para eso podemos aplicar los siguientes conceptos a clases.
SELLADO, ABSTRACCIÓN Y VIRTUALIDAD
Previamente vimos que `partial` significa que podemos dividir la declaración
de la clase en varias partes, pero existen otros modificadores que se
le pueden poner a una clase, como por ejemplo:
los 3 conceptos mencionados, estos son parte de la programación orientada a objetos,
estos nos permiten definir con mejor claridad cuál queremos que sea el uso de una
clase que nosotros estemos declarando.
Para esto vamos a empezar con la palabra clave `abstract`, esta nos permite crear
una clase la cual contiene implementaciones abstractas de la funcionalidad final
de nuestra clase, como por ejemplo "animal" sería una forma abstracta de llamar
a un perro. Ya que este es realmente un animal, y eso lo comparte con
muchas otras especies.
Entonces, nosotros podemos abstraer todos esos conceptos y dejar que otros
usuarios hagan esas implementaciones, como el caso de que estemos creando una
librería, en el caso de SFML que es una librería para crear videojuegos,
esta tiene una clase abstracta llamada `Drawable`, esa abstrae el poder
dibujar vertices en una pantalla.
Las clases abstractas no se pueden instanciar, la única forma que tenemos
de interactuar con estas es mediante herencia.
Como ejemplo del mundo real para lo anterior podríamos decir que no existe
un "animal" pero si un perro, este es una entidad que deriva de "animal"
o por ejemplo, no deberíamos ser capaces de crear un vehículo, pero si un
coche, entonces, en el ejemplo anterior hubiera estado bien hacer que
vehiculo fuese abstracto.
Vamos a declarar una clase abstracta.
*/
public abstract partial class Animal // esta clase solo se puede heredar.
{
/*
Primero, vamos a pensar que tienen en común todos los animales,
vamos a decir que todos hacen ruido, pero todos hacen uno de diferente.
Como vimos antes si nosotros hacemos una conversion al tipo
padre de una clase, no vamos a tener acceso a las funciones
declaradas en las clases que hereden de esta.
La diferencia aquí es que dentro de una clase abstracta, podemos
hacer la declaración de este método o propiedad, si hemos hecho un cast
al tipo padre vamos a poder acceder a este miembro de todas formas
y obtener un valor distinto dependiendo de la implementación
del tipo original.
Vamos a crear un método abstracto que sea `MakeSound` donde
vamos a permitir hacer sonido, después haremos ejemplos
de como podemos instanciar cada tipo.
*/
// este no tiene cuerpo, las implementaciones van en clases derivadas.
public abstract void MakeSound();
}
public sealed class Dog : Animal
{
/*
Hacer una clase sealed no es obligatorio, pero con eso
estamos indicando que no queremos que nadie herede esta clase,
por ejemplo, `string` es una clase sellada, no puedes heredarla.
Cuando heredamos `Animal` estamos obligados a implementar `MakeSound`,
ya que este no tiene una implementación en la clase padre.
Para implementar un miembro especificado como abstract en una implementación
de la clase abstracta usamos `override`.
Los unicos miembros que pueden ser `abstract` son propiedades y métodos.
*/
public override void MakeSound()
{
Console.WriteLine("WaW!");
}
}
public sealed class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine("Miau!");
}
}
/*
Ahora podemos hacer lo siguiente
Animal dog = new Dog();
Animal cat = new Cat();
dog.MakeSound(); // "WaW!"
cat.MakeSound(); // "Miau!"
A pesar de que `dog` y `cat` sean del mismo tipo, `Animal`, tenemos
acceso al método `MakeSound`, ya que este está especificado
de forma abstracta en la clase principal `Animal`.
De todas formas, aunque tengamos las variables de tipo `Animal`, cada
variable es de un tipo distinto, respectivamente `Dog` y `Cat`, por eso
si llamamos `MakeSound` los métodos harán cosas distintas.
Luego existen los miembros virtuales, estos son miembros a los
cuales podemos hacerles override sin tener que estar en una clase abstracta.
Un ejemplo de esto es el `ToString`, `ToString` es un método que declara
`object`, por lo que todas las clases derivadas lo tienen, pero como
`ToString` es virtual, entonces podemos hacer override de esta opcionalmente.
Por defecto `ToString` devuelve el nombre del tipo o sea `GetType().ToString()`,
eso lo veremos posteriormente.
Si nosotros no queremos que haga eso, podemos hacer override al miembro y
por ejemplo mostrar todas las propiedades de su clase contenedora.
Para este ejemplo vamos a usar la clase de `Person` y vamos a hacer `override`
al `ToString` del `object` que deriva.
*/
// Esta clase hereda de `object`, todas lo hacen por defecto, también implementa `IParsable<Person>`.
public partial class Person
{
// `ToString` está declarado en `object`
public override string ToString()
{
// Si queremos acceder al comportamiento base, usamos la palabra clave `base`
// eso nos devolvería el anteriormente mencionado `GetType().ToString()`,
// como en este caso el `ToString` de `Type` devuelve `"Type: " + this.Name` lo que
// vendria siendo el nombre del tipo concatenado a `"Type: "`, entonces `b`
// ahora tiene ese valor.
string b = base.ToString();
// `base` como keyword es un puntero a la clase base, aunque sea otra propiedad
// la que estemos referenciando, si a esta le hicimos override, acceder desde `base`
// hará que accedamos a la implementación base y no al override.
// También podemos hacer return a una string que describa las propiedades de la clase.
return $"Age: {Age}\nName: {Name}\nHealth: {Health}";
}
}
/*
Si un miembro no es virtual, también podemos hacerle override, pero no con la palabra clave
override, sino con la palabra clave `new`.
Para la siguiente demostración crearemos una clase `Police`, podríamos hacer que un policia
al tener el cinturón de armas tenga 3 armas en vez de 2,
Para eso vamos a declarar un nuevo enum con las propiedades correspondientes.
*/
public enum PoliceEquippedWeapon
{
Primary,
Secondary,
Tertiary
}
// En este caso reharemos prácticamente toda la implementación.
// Este tipo de implementación no es recomendado, si se ha de hacer, se
// debe plantear hacer el programa con otra estructura, esto solo lo haríamos
// en el caso en que no tengamos acceso a la declaración de los miembros.
public sealed class Police : Person
{
// Implementamos el constructor, lo cual es obligatorio a no ser que haya
// uno de vacío.
public Police(string name, DateTime? birth = null) : base(name, birth) { }
// Como _equippedWeapon es una propiedad privada de Person, simplemente
// dejamos de usar esa y usamos otra de nueva con un tipo distinto.
private PoliceEquippedWeapon _equippedWeapon = PoliceEquippedWeapon.Primary;
// Declaramos todas las armas de nuevo, dejando las anteriores inútiles.
private IWeapon? _primaryWeapon;
private IWeapon? _secondaryWeapon;
private IWeapon? _tertiaryWeapon;
// Lo mismo aquí, tampoco nos interesa usar el GetWeapon de base sea protected.
public ref IWeapon? GetWeapon()
{
// En este caso no podemos usar una expresión switch, porque los valores
// son por referencia, pero podemos extender la expresión `ternary`.
return ref _equippedWeapon == PoliceEquippedWeapon.Primary
? ref _primaryWeapon
: ref _equippedWeapon == PoliceEquippedWeapon.Secondary
? ref _secondaryWeapon
: ref _tertiaryWeapon;
}
// Aquí podemos usar new, ya que `SwapWeapon` es public, tenemos acceso desde aquí.
public new void SwapWeapon()
{
/*
Esto es una expression switch, en este caso,
en vez de "hacer algo", retorna el valor.
*/
_equippedWeapon = _equippedWeapon switch
{
// Si _equippedWeapon es Primary, asignamos Secondary a _equippedWeapon.
PoliceEquippedWeapon.Primary => PoliceEquippedWeapon.Secondary,
// Si _equippedWeapon es Secondary, asignamos Tertiary a _equippedWeapon.
PoliceEquippedWeapon.Secondary => PoliceEquippedWeapon.Tertiary,
// Si _equippedWeapon no es ninguna de las anteriores, asignamos Primary.
// El _ actua como un `default` en un `switch` normal.
_ => PoliceEquippedWeapon.Primary
// La asignación pasa porque arriba estamos haciendo
// `equippedWeapon = (... switch ...)
};
}
}
/*
La diferencia como ejemplo es que usar `override` dice claramente "¡Hey, estoy
reemplazando este método de la clase base!", mientras que `new` es más como "Bueno,
tengo un método con el mismo nombre, pero no te preocupes, no es exactamente lo mismo
que el de la clase base".
Cuando usas `override`, estás asegurando que el método en la clase derivada será invocado
según el tipo real del objeto en tiempo de ejecución. Es decir, te da las propiedades del polimorfismo,
donde el método correcto se elige dinámicamente. Pero con `new`, eso no sucede. El método de la clase
base es ignorado cuando se llama a través de un objeto de la clase derivada.
Eso no quita que podamos usar la palabra clave `base` en un método al que se le hizo `new`.
TIPOS GENÉRICOS
Anteriormente vimos lo que es `IParsable<T>`, `IParsable` es una interfaz, pero `<T>`, ¿Qué es `<T>`?
En el caso de `IParsable` este define un método que cambia de tipo dependiendo de lo que le
estemos pasando en `<T>`, por ejemplo, si le pasamos un int, entonces este nos obligara
a implementar `int Parse(string, ICultureProvider)` y los demás... En este caso vemos como
el método a implementar cambia dependiendo del tipo genérico que le pasemos.
Los tipos genéricos pueden declararse en métodos, clases, interfaces, structs y records,
nosotros también podemos declarar el nuestro, por ejemplo, para un método que
sirva para convertir JSON a un tipo específico, si queremos hacer que este
mapee todas las propiedades JSON a propiedades de C# la declaración del método sería la siguiente.
*/
public static class JsonParser
{
public static TTarget? Deserialize<TTarget>(string payload)
{
/*
En este caso como deserializar JSON es un poco complejo,
simplemente lo omitiremos, a lo que vamos.
Ahora nosotros tenemos `TTarget` como un tipo, este tipo está
vacío, ya que no tenemos ninguna información del tipo que nos van
a pasar.
Nosotros podemos crear una variable de tipo `TTarget`
si es que la oportunidad nos lo permite, también convertir
otro tipo a `TTarget` en este caso y tener parámetros
del tipo `TTarget`.
Podemos tener más de un parámetro de tipo o tipo genérico,
simplemente pondríamos una `, ` y poner otro parámetro
de tipo, por ejemplo
class KeyValuePair<TKey, TValue> {}
Al invocar esa clase debemos especificar los parámetros
de tipo genérico como
new KeyValuePair<string, string>()
Cuando nosotros llamemos a ese método, tendremos que especificarlo,
en este caso sería `JsonParser.Deserialize<CustomClass>("...")`,
donde `CustomClass` sería una clase que hemos declarado previamente.
En el caso de que tuviésemos un método como
GetFromAnyType<T>(T value) {}
No tendríamos que especificar el tipo al llamarlo, ya que
al pasarle un parámetro, C# infiere que el tipo es el tipo
del valor que le pasamos.
En este caso, se podria llamar `GetFromAnyType(10)` y c#
detectaria automáticamente que eso es un `int`, a no ser
que queramos otro tipo con el mismo literal, luego
deberíamos o especificarlo o convertir el valor si no
deseamos usar la inferencia automática.
*/
// Hacer `return default` te da el defecto del tipo que sea,
// por ejemplo de `int` sería 0 de `Nullable<int>` sería `null`.
// También podemos obtener el valor null de un tipo con `default(int)`
// y eso nos devolvería 0
// ver: https://learn.microsoft.com/es-es/dotnet/csharp/language-reference/operators/default.
return default;
}
/*
Con un tipo genérico realmente no podemos hacer mucho por defecto,
pero si le especificamos restricciones al C# tener la información
sobre lo que queremos hacer con ese tipo nos da más libertad al
manipularlo.
Para hacer eso usamos la palabra clave `where`, con esa palabra clave
podemos decir "donde T herede de X", "donde T herede de X e implemente Y",
y aparte de eso alguna otra restricción como "class" que haría
que solo podamos pasar clases y no structs en el parámetro de tipo.
Vamos a dar algún ejemplo:
where T : SomeType, ISomeInterface // donde T herede SomeType e implemente ISomeInterface.
where T : class, SomeType // donde T sea una clase y herede de SomeType
Luego tenemos varios tipos más de restricciones como por ejemplo, `unmanaged` que significa que no
es un tipo el cual `CIL` toma en cuenta para la limpieza de recursos o `new()` que significa
que el tipo genérico tiene un constructor sin parámetros, en el caso de la restricción
`struct` esta no puede ser combinada con `class`, `new()` o `unmanaged`, luego
`class` no puede ser combinada con `struct` y por último `unmanaged` no puede ser
combinado con `struct` o `new()`
ver: https://learn.microsoft.com/es-es/dotnet/csharp/programming-guide/generics/constraints-on-type-parameters
para más información sobre las restricciones de tipo genérico.
Para aplicar una restricción lo hacemos de la siguiente manera:
*/
// donde TTarget sea una clase (o sea un tipo de referencia) implemente IParsable y tenga un constructor vacío.
public static bool IsDeserializable<TTarget>(string payload) where TTarget : class, IParsable<TTarget>, new()
{
// la restricción `new()` nos habilita a poder crear una instancia del tipo.
TTarget target = new();
// como implementa IParsable<TSelf> podemos hacer TTarget.Parse.
TTarget.Parse(payload, null);
// En conclusion, las restricciones de tipo genérico nos dan información sobre
// un tipo y nos permiten acceder a miembros de estas, ya que tenemos la certeza
// de que ese miembro o comportamiento al que tratamos de acceder existe.
// la implementación del método es irrelevante.
return default;
}
}
/*
MÉTODOS DE EXTENSION
Los métodos de extension son métodos que podemos escribirles a clases aunque no tengamos
acceso a esa instancia, cuando escribimos un método de extension tampoco podemos acceder
ni a los métodos privados ni protegidos de esa clase, solo a los públicos, como su
nombre indica es solo un método de extension y no sirve para hacer cambios grandes
a una clase.
Para crear un método de extension requerimos una clase estática donde escribiremos
la definición de nuestro método de extension, y un método que
queramos que haga algo a una instancia mediante un parámetro, la diferencia de este
es que le pondremos `this` al principio del parámetro.
Como los métodos de extension requieren que pasemos un parámetro como instancia a modificar,
por lógica no podemos crear un método de extension a una clase estática.
Vamos a crear un método de extension que nos permita juntar un array de strings
a un solo string con un separador.
*/
public static class StringExtensions
{
// Definimos un método de extension llamado Join que toma un separador
// como segundo parámetro y devuelve un string.
public static string Join(this string[] collection, string separator)
{
// Declaramos un string que va a ser nuestro resultado.
string result = string.Empty;
// Por cada elemento en `collection` o sea, la instancia
// sobre la que llamar el método de extension.
foreach (string item in collection)
// añadimos el item y el separador.
result += $"{item}{separator}";
// Devolvemos el resultado, pero quitamos el último separador
// [(.. = desde el principio hasta)(^separator.Length = la longitud del separador contando desde el final)]
return result[..^separator.Length];
}
}
/*
De esta forma podemos llamar `Join(", ")` desde una instancia de `string[]` de la
siguiente forma
string[] array = ["hola", "que", "tal"];
string result = array.Join(" "); // "hola que tal"
El comportamiento anterior lo podemos obtener con `string.Join` como método estático de la
clase `string`
result = string.Join(array, " ");
TIPOS UNMANAGED Y PUNTEROS DE MEMORIA
En C# hay un concepto llamado colector de basura o `garbage collector` que desaloja toda la
memoria que se deja de usar, o deja de haber acceso a ella, pero hay ciertos tipos
los cuales este colector no tiene en cuenta, como los `unmanaged` y los tipos unsafe
o sea aquellos que cuentan con punteros de memoria y administración de memoria por
parte del usuario del lenguaje.
En c# existe el tipo nint que es la palabra clave para el tipo `IntPtr`, o sea una dirección
de memoria, esta también puede ser asignada con un literal numérico, pero el cómo funciona
su asignación es un poco distinto.
En su forma más básica un puntero de memoria es una variable que apunta a una dirección
de memoria explícitamente asignada.
La forma más común de obtener direcciónes de memoria es alojando esta, cuando alojamos memoria
hablamos de bytes, ya que las celdas de memoria en la RAM están divididas por bytes.
Para alocar memoria lo hacemos con Marshalling en el caso de C#, y podemos acceder a esta
funcion a traves de la api de Marshall también conocido como System.Runtime.InteropServices.Marshall
ver: https://learn.microsoft.com/es-es/dotnet/api/system.runtime.interopservices.marshal?view=net-8.0
Cuando nosotros alojamos memoria obtenemos un IntPtr en la dirección de memoria que nos ha asignado
el sistema para guardar lo que sea que tengamos que guardar.
Un puntero actua como un iterador dentro del array masivo de memoria, sabiendo que un `IntPtr`
es un valor referenciando una dirección de memoria si nosotros sumamos 1 a este, iremos a la siguiente
dirección de memoria o en este caso como son 4 bytes se movería a los próximos 4 bytes.
Para movernos en estructuras de forma más cómoda podemos castear el `IntPtr` a un puntero
de otro tipo que sea unmanaged, previamente vimos el operador `*` como un operador
binario, pero ahora lo veremos como un operador unario, donde nos permite llegar
al valor de un puntero o al puntero de un valor.
Teniendo en cuenta que un long son 8 bytes si nosotros casteamos el `IntPtr` a un `ulong *`
significando que eso es un puntero que apunta a `ulong` cuando queramos sumar al puntero
nos moveremos de 8 en 8.
En cuanto a punteros, tenemos la capacidad de movernos donde sea con un puntero, también
podemos escribir donde sea, pero no deberíamos, ya que si asignamos un valor a una dirección
de memoria crítica y el sistema no lo tiene en cuenta nos podemos cargar la RAM o cualquier
otra cosa que esté haciendo uso de esa memoria.
Cuando digo `podríamos` o `podemos llegar` es porque no siempre es asi, en muchos casos
te puede dar un error, en otros simplemente no pasaría nada, y en otros te puedes cargar
el sistema, eso se conoce como undefined behavior o nasal demons.
Por este hecho cuando tocamos punteros lo tenemos que hacer en un entorno `unsafe`,
el cual no todos los proyectos aceptan y su aceptación se ha de especificar explícitamente
en el csproj
ver: https://learn.microsoft.com/es-es/dotnet/csharp/language-reference/compiler-options/language#allowunsafeblocks.
Ahora vamos a hacer un wrapper de Marshall para entender más o menos como funciona alocar memoria
de una manera más objetiva.
*/
// Para usar punteros debemos usar la palabra `unsafe` en el contexto
// esta también se puede aplicar a un método o cuerpo vacío.
public static unsafe class MAlloc
{
// Definiremos una funcion que devuelva un puntero a una dirección
// de memoria donde haya disponible el tamaño especificado en el
// parámetro `size`, este devolverá un puntero del tipo
// que sea especificado en el parámetro de tipo,
// teniendo en cuenta que el parámetro de tipo tiene la restricción
// de `unmanaged`.
public static TMemory *Alloc<TMemory>(int size) where TMemory : unmanaged
{
// Marshal.AllocHGlobal nos devuelve un IntPtr de la posicion
// inicial del puntero, teniendo la seguridad de que
// los próximos `size` bytes estaran disponibles para
// nosotros escribir recursos ahí.
return (TMemory *)Marshal.AllocHGlobal(size);
}
/*
Cuando asignemos memoria a un sitio, esta no será tomada en cuenta
por el colector de basura del lenguaje, por lo que tendremos que
limpiarla manualmente, para eso tendremos que pasarle el puntero
inicial a `Marshal.FreeHGlobal(IntPtr)`.
*/
public static void Free<TPtr>(TPtr* ptr) where TPtr : unmanaged
{
// Le pasamos el puntero convertido a un IntPtr.
Marshal.FreeHGlobal((IntPtr)ptr);
}
/*
Para alocar memoria explícitamente tenemos que decirle el tamaño real
de la memoria que queremos alocar, por ejemplo si queremos alocar un
long, deberemos alocar 8 bytes.
Si queremos alocar 2 longs, serían 8 * 2 bytes (tamaño * cantidad).
Para obtener el tamaño de un tipo podemos usar el operador `sizeof()`
siendo este una palabra clave al mismo tiempo, dentro de los paréntesis
ponemos el tipo del que queramos obtener el tamaño, solo podemos obtener
el tamaño de un tipo unmanaged.
sizeof(int) == 4
sizeof(long) == 8
sizeof(short) == 2
entonces en este caso podríamos hacer
long *ptr = MAlloc.Alloc<long>(sizeof(long) * 2)
También si queremos basarnos en el tipo, podemos hacer otra implementación.
*/
public static TMemory* AllocSize<TMemory>(int size) where TMemory : unmanaged
{
return Alloc<TMemory>(size * sizeof(TMemory));
}
/*
En el caso de ejecutar AllocSize más que tamaño debemos especificar cantidad
de instancias del tipo.
También debemos tener en cuenta que podemos movernos en el puntero y escribir recursos en él.
Para eso declararemos una funcion de demostración donde podamos hacer todos los ejemplos.
*/
public static void PointerExample()
{
// Un puntero con sizeof(long) * 3 bytes.
long *ptr = AllocSize<long>(3);
// Si hacemos `Console.WriteLine()` a `ptr` veremos
// una dirección de memoria como por ejemplo `0x7fff5faff718`
// para obtener una referencia a la dirección
// de memoria donde podemos asignar un valor u obtenerlo
// usaremos de nuevo `*`, entonces
long value = *ptr;
// En este caso como aquí no hay ningun valor,
// él por defecto de long es 0, asi que obtendremos 0.
// Entonces que para asignar un valor haremos lo siguiente.
*ptr = 10L; // 10L representa un Long con el valor 10.
// Para movernos a la siguiente dirección de memoria, incrementaremos el puntero,
// como este es un long, se moverá 8 bytes a la derecha.
ptr++;
// Si la dirección antes era 0x7fff5faff718 ahora es 0x7fff5faff720
// ya que se le añadió 8.
*ptr = 20L;
// Una representación gráfica de lo que hay aquí en decimal sería [10, 20, 0].
// Existe otra forma de alocar memoria sin tener que mover el puntero, y eso
// es con el operador de índice o sea [].
ptr[1] = 30L; // asignamos 30L a +8 bytes de la posicion actual.
// Ahora nos quedaríamos con [10, 20, 30].
// IMPORTANTE SIEMPRE LIBERAR LA MEMORIA.
Free(ptr - 2); // ptr - 2 posiciones.
// El runtime sabe cuanta memoria de alocar porque lo
// toma en cuenta cada vez que alojamos memoria.
}
/*
El uso o dependencia de una clase unsafe no implica que su dependiente
deba ser unsafe.
Si tenemos un struct el cual solo tenga fields y propiedades de
tipos unmanaged, este también se podria considerar un unmanaged type,
por lo que podríamos crear punteros u obtener el tamaño de este mismo.
Para lo anterior debemos usar Marshal.StructureToPtr() y
viceversa Marshal.PtrToStructure()
ver: https://learn.microsoft.com/es-es/dotnet/api/system.runtime.interopservices.marshal.structuretoptr
ver: https://learn.microsoft.com/es-es/dotnet/api/system.runtime.interopservices.marshal.ptrtostructure.
Lo mismo aplica a arrays.
*/
}
/*
ESPACIOS DE NOMBRE
Los espacios de nombre o `namespaces` son secciones de código
con los que separamos partes de nuestro proyecto, por ejemplo
`Console` es una API que viene de `System` que es un espacio
de nombre como muchas otras.
Para declarar un espacio de nombre lo podemos hacer de la siguiente manera
*/
namespace Tutorial
{
// Aquí pueden ir clases y cualquier tipo de `top level statement`,
// pero no otro `namespace`
}
/*
Si quisiésemos acceder a una clase declarada dentro de ese namespace
lo haríamos como si quisiésemos obtener un método de una clase estática
Tutorial.NombreDelTipo
De espacios de nombre con `{}` puede haber varios en un
archivo, pero también los podemos declarar `por archivo`
sin tener que encapsular las clases de ese archivo
en un `{}` y lo haríamos de la siguiente manera
namespace Tutorial;
Pero si hacemos eso, no podremos declarar ningun otro espacio de nombre
en el mismo archivo aunque sea cerrado con `{}`.
Los espacios de nombre siguen una regla, que es nombrarlos con base en
los directorios.
Cuando declaremos un espacio de nombre podemos escribir `Tutorial.Utiles`
y eso es un nombre de espacio válido.
Entonces, el primer nombre sería el nombre del proyecto, y posteriormente
escribimos cada carpeta.
Si tuviésemos un archivo dentro de la carpeta `BaseDeDatos` que a la vez
está dentro de la carpeta `Utiles`, como el proyecto se llama `Tutorial`
el espacio de nombre sería el siguiente
namespace Tutorial.Utiles.BaseDeDatos;
Esta última regla es simplemente una recomendación, también es
la configuración por defecto de Rider y Visual Studio
ver: https://learn.microsoft.com/es-es/dotnet/standard/design-guidelines/names-of-namespaces
ver: INGLÉS https://stackoverflow.com/questions/4664/should-the-folders-in-a-solution-match-the-namespace
PROYECTOS Y ENSAMBLAJES
Esta seccion no contiene código, simplemente habla sobre como se administran
los proyectos en C#, para ello.
PREPROCESADOR
Como sabemos C# es un lenguaje compilado, cuando hacemos `build` y ejecutamos
nuestro programa, el programa que ejecutamos no es C#, es IL, el IL
es un lenguaje generado por el compilador de más bajo nivel,
convirtiendo asi C# código por pasos, quitando toda esa abstracción
que tiene el lenguaje.
A ese compilador nosotros le podemos instruir como queramos que se
genere nuestro programa, C# tiene distintas versiones del lenguaje
por ejemplo el hecho de inicializar los array `[1, 2, 3]` asi es sumamente nuevo.
Por lo que podemos definir una directiva de preprocesador que mire la version del
lenguaje que estamos usando.
*/
public static class PreProcessorTests
{
static PreProcessorTests()
{
#if NET8_0_OR_GREATER
// Este código va a compilar si la version de .NET es la 8.0.
int[] array = [1, 2, 3, 4];
#else
int[] array = {1, 2, 3, 4};
#endif
// También tenemos la opción de mirar si la APP está en modo `debug` o `production`.
#if DEBUG
Console.WriteLine("Modo debug!");
#else
Console.WriteLine("Modo produccion!");
#endif
/*
A lo anterior se le llama compilación condicional, existen los bloques
#if (DEFINE_CONST || DEFINE_CONST && DEFINE_CONST)
#elif (DEFINE_CONST || DEFINE_CONST && DEFINE_CONST)
#else
#endif
También tenemos la opción de declarar símbolos personalizados basados en
otras condiciones.
Un `#define` solo puede ir al principio del archivo, su sintaxis es la siguiente
#define TOKEN
Al contrario de C o C++ los `#define` no pueden tener un valor,
solo podemos comparar su existencia.
Antes hablamos un poco de null-habilidad, eso se puede habilitar
o deshabilitar, todas las warnings relacionadas sobre el tema de sí algo es
null o no se pueden alternar.
*/
#nullable enable // habilita las warnings
#nullable disable // deshabilita las warnings
#nullable restore // habilita/deshabilita las warnings basado en la configuración del proyecto.
/*
Con declaraciones de preprocesador también podemos definir
regiones en nuestro código para compilarlas condicionalmente
o para simplemente expandirlas o contraerlas en nuestro editor
de código.
Eso puede hacer más legible un archivo o seccion de código.
*/
// el nombre hace que la region sea más descriptiva.
#region TestRegion
#endregion
/*
También podemos decirle al compilador que genere un warning o
error si una condicion de preprocesador se cumple.
*/
#if !DEBUG
#error Esta seccion solo puede ser ejecutada en modo DEBUG.
#endif
/*
Si ahora tratamos de compilar en modo `Release` o producción no
vamos a poder ya que habrá un error.
Lo mismo pasa con los warnings
*/
#if !DEBUG
#warning Esta seccion deberia ser solo ejecutada en modo DEBUG.
#endif
/*
A diferencia de un error un warning si nos dejara compilar.
Antes definimos una region, pero también podemos cambiar nombres de
líneas.
*/
// Esto nos permite cambiarle el nombre a la línea.
#line 3079 "Hola"
// Esto ocultarla
#line hidden
// Esto volver a la configuración por defecto.
#line default
/*
Lo anterior en muchos editores no funciona, porque seamos sinceros...
¿Por qué alguien querría esto? XD
En fin... En C# hay un montón de warnings relacionados con calidad de código
cosas redundantes, es un estándar que hay en C#, pero podemos usar pragma
para deshabilitar eso, las directivas de pragma son las siguientes
#pragma warning (disable|enable) (#CS(codigo), $CS(codigo)?, ...)
#pragma warning (disable|enable) (#CS(codigo), $CS(codigo)?, ...)
Eso lo podemos ver al principio del archivo, hay muchas de deshabilitadas en esta guía
ver: https://learn.microsoft.com/es-es/dotnet/csharp/language-reference/compiler-messages/
ver: https://learn.microsoft.com/es-es/dotnet/csharp/language-reference/preprocessor-directives.
Cuando definimos una declaración de preprocesador, esta no se vera reflejada
en el programa final.
*/
}
}
/*
IMPLEMENTACIONES ESPECÍFICAS DEL LENGUAJE Y BCL.
Existen casos especiales de interfaces o clases que podemos implementar o heredar
en una clase nuestra para que esta tenga un comportamiento "especial", por ejemplo,
poder ser iteradas por un foreach, o ser utilizables como atributo en el caso de [Flags]
en los enums.
Las siguientes clases implementan funcionalidades que no se pueden replicar
sin extender estas mismas e implementan palabras clave y comportamientos
los cuales no podemos programar de por sí.
Vamos a empezar con los iterables.
Existen 2 interfaces que pueden servir para hacer que nuestra
clase sea iterable a traves de un foreach, estos son IEnumerator<T>
y IEnumerable<T>.
El cómo funcionan los iteradores es de la siguiente manera, de
forma muy abstracta estos implementan un método llamado `next()`
y un método llamado `current()`, el método `next()` retornara
true o false dependiendo de si existe o no un siguiente valor,
si este existe va a seguir iterando, si no va a parar el loop
y seguir con el resto del código. El método `current()` nos devolvería
el valor actual una vez hayamos hecho next(). Por ejemplo si tenemos
[1, 2, 3, 4] y desde -1 hacemos next() nos devolverá true porque
el valor 0 existe, luego obtenemos ese valor y hacemos lo que sea.
Una vez hecho eso llamamos next de nuevo para llevarnos al índice 2,
y asi consecutivamente hasta que `next()` devuelva false.
Vamos a hacer una implementación de muestra.
*/
// Esta clase usa un constructor primario e implementa IEnumerable<int>
// o sea que el valor que recibimos por cada iteración es un `int`
public class FibonacciSequence(int limit) : IEnumerable<int>
{
// Implementamos el método que ha de devolver un enumerable,
// ya que eso es lo que toma un iterador.
public IEnumerator<int> GetEnumerator()
// Devolvemos una clase que posteriormente creamos
// la cual implementa IEnumerator<int>
=> new FibonacciEnumerator(limit); // FibonacciEnumerator toma el parámetro `limit`
// Implementamos la version no genérica de `GetEnumerator`
// explícitamente especificando que es de `IEnumerable` sin
// tipo genérico.
IEnumerator IEnumerable.GetEnumerator()
// Devolvemos GetEnumerator cuyos valores
// se convierten implícitamente a object.
=> GetEnumerator();
// Creamos una clase que implemente IEnumerator con
// un constructor primario que de nuevo tome un límite.
private class FibonacciEnumerator(int limit) : IEnumerator<int>
{
// Para saber más sobre la secuencia Fibonacci
// ver: https://en.wikipedia.org/wiki/Fibonacci_sequence.
// Declaramos un _previous y un _index para seguir
// el rastro de los siguientes números
private int _previous = 1;
private int _index;
// El Current lo declaramos como una auto propiedad
// la cual se usa para seguir el rastro del número actual
// y también actuaria como el método `current()` en el `IEnumerator`
public int Current { get; private set; } = 1;
// Implementamos la version no genérico de `Current` la cual
// devuelve el mismo objeto que `Current` pero convertido implícitamente
// a `object`
object IEnumerator.Current => Current;
// El método `MoveNext()` seria el equivalente de `next()`
public bool MoveNext()
{
// Si el index actual es mayor que el límite
// devolvemos false.
if (_index > limit)
return false;
// Si _index es 0 o 1 sumamos 1 a index y
// retornamos true ya que existe un siguiente valor.
if (_index is 0 or 1)
{
_index++;
return true;
}
// Hacemos los pasos del algoritmo.
int next = Current + _previous;
_previous = Current;
Current = next;
// Sumamos 1 a index y devolvemos true.
_index++;
return true;
}
// Hay enumerables que implementan reset
// y hay otros que no, en el caso de C#,
// si lo hace.
public void Reset()
{
// Devolvemos todas las variables
// a su valor inicial.
Current = 1;
_previous = 0;
_index = 0;
}
// IEnumerator implementa IDisposable
// el cual te obliga a implementar este
// método que sirve para liberar recursos,
// pero en nuestro caso no lo necesitamos.
public void Dispose()
{
// Más adelante hablaremos de lo que significa eso.
GC.SuppressFinalize(this);
}
}
}
/*
Ahora tenemos la capacidad de hacer la secuencia de `Fibonacci`
con un `foreach` de la siguiente manera:
foreach (int actual in new FibonacciSequence(50))
{
Console.WriteLine(actual);
}
Esto imprimiria cada número de la secuencia fibonacci hasta el índice 50.
Aparte de eso existe alguna que otra interfaz como IDisposable, que
nos habilita a poder especificar explícitamente al colector de basura
si limpiar los recursos de un objeto o no dependiendo
de lo que tengamos que hacer con este objeto.
Para eso crearemos una clase que implemente IDisposable.
*/
// La clase implementa IDisposable.
public class DisposableClass : IDisposable
{
// Implementamos el método `Dispose`
// que se va a llamar cuando le pidamos
// al colector de basura limpiar este objeto de
// la memoria.
public void Dispose()
{
// Llamar a GC.SuppressFinalize no es
// obligatorio, pero es recomendado y este
// hace que el CLI no llame al finalizador
// para esta clase. En este caso nos servira
// para que no se limpien los recursos automáticamente
// y podamos limpiarlos nosotros asi evitando
// ObjectDisposedException y otros problemas derivados.
GC.SuppressFinalize(this);
// Si tuviésemos un Stream o cualquier otra
// cosa que no se limpiase automáticamente
// siendo parte de esa clase como un campo
// o auto propiedad, podríamos llamar
// `<object>.Dispose();` aquí.
}
}
/*
Si tuviéramos un objeto de `DisposableClass` podríamos
hacer uso de `using` para que llame a `Dispose` en cuanto
el recurso este fuera de alcance, por ejemplo, usándolo dentro
de un IF.
if (...)
{
using DisposableClass obj = new();
// asumiendo que DisposableClass tiene métodos y otras cosas
obj.UnMetodoQueHaceCosasMuyImportantes();
}
// Al llegar aquí se habría llamado al `Dispose()` de `obj`.
También existe otra dispose pattern que son los destructores,
estos se pueden declarar en cualquier clase independientemente
de si implementa IDisposable o no lo hace.
Un destructor se declara de la siguiente forma
*/
public class DestroyableClass
{
// Usamos el mismo operador que `~` invertir bits
// delante del nombre de la clase y lo declaramos
// como un método con su respectivo cuerpo.
~DestroyableClass() // Este no debe tener parámetros.
{
// Aquí podemos liberar todos los recursos que deseemos.
}
}
/*
La diferencia que tiene un destructor de implementar IDisposable
es que no tenemos el control sobre cuando se va a limpiar
los recursos del objeto, este va a seguir las reglas
de limpieza definidas por el CLI.
Como clases especiales también tenemos `Exception`
que permite que nuestra clase sea `Throwable`,
vamos a dar un ejemplo práctico de eso.
*/
// En muchos casos las excepciones son sealed porque
// no tiene mucho sentido extender una excepción, ya que
// la cantidad de información que contienen es fácilmente
// reproducible o para casos muy específicos.
public sealed class CustomException(int code) : Exception
{
// Message es la descripción del error.
public override string Message { get; } = $"El proceso personalizado dejo de ejecutarse con código: {code}";
// Source es de donde sale el error.
public override string? Source { get; set; }
// El `StackTrace` sirve para rastrear de donde salió
// el error.
public override string? StackTrace { get; } = null;
/*
Existen otras propiedades virtuales dentro de `Exception`
ver: https://learn.microsoft.com/es-es/dotnet/api/system.exception?view=net-8.0
Pero esas son las principales, muchas veces hay propiedades a las cuales
no queremos hacerles override, ya que su valor por defecto nos da la información
que necesitamos, como el caso de `StackTrace`, hay pocos casos en donde
haríamos override a `StackTrace` como por ejemplo si estuviéramos
haciendo un lenguaje de programación interpretado, entonces
probablemente tendria sentido, pero son casos muy específicos.
*/
}
/*
El haber declarado esa clase nos da acceso a poder hacer un `throw` a esta misma clase,
el constructor se ejecuta antes de cualquier cosa y se construye como una clase normal,
una vez esté construida el CLI va a generar un error y lo va a mostrar por consola
y posteriormente va a finalizar la ejecución del programa.
En este caso con `CustomException` podemos hacer lo siguiente
throw new CustomException(5);
El 5 viene de que hemos especificado en el constructor que queremos un valor
entero para nuestra clase. Cabe mencionar que en un constructor primario no podemos validar
el código de error.
En cuanto a metadatos para tipos podemos extender una clase llamada `Attribute`,
la cual nos permite instanciar-la en una lista de atributos.
Vamos a dar un ejemplo práctico de eso.
*/
// Este atributo es especial, nos permite definir donde queremos que
// se pueda usar el atributo.
[AttributeUsage(AttributeTargets.Class)]
public sealed class EnumerateClassAttribute(int number, string description) : Attribute
{
// Exponemos estas propiedades computadas internamente.
internal int Number => number;
internal string Description => description;
}
/*
Ahora si creamos una clase podemos especificarle este atributo,
los atributos de por sí no sirven para mucho, para acceder a ellos
y que estos tengan un sentido se hace a traves de `reflections`, pero eso
lo veremos más adelante.
*/
// Cuando invocamos un atributo, podemos omitir el `Attribute`.
// Para instanciar este no hace falta la palabra clave new,
// como anteriormente especificamos que ese atributo
// solo puede ser usado en clases, si lo usamos en cualquier otro sitio
// nos va a dar un error.
[EnumerateClass(10, "Hola :)")]
public class SomeClass;
/*
En cuanto a la clase de `AttributeUsageAttribute` tiene 3 posibles parámetros
que son opcionales.
Tenemos el enum de `AttributeTargets` que dice donde el atributo se puede usar.
Tenemos el bool de `Inherited` que especifica si al heredar esa clase, el atributo
se queda en la clase hijo o no le afecta a esta.
Y finalmente tenemos el `AllowMultiple` que define si se puede especificar
más de una vez el mismo atributo en el mismo miembro
ver: https://learn.microsoft.com/es-es/dotnet/api/system.attributeusageattribute?view=net-8.0.
Los atributos los veremos más adelante en el apartado de `reflections`.
En cuanto a otras implementaciones especiales tenemos `Task<T>` que sirve para
trabajar de forma asincrònica.
Un ejemplo práctico del uso de Task sería una tarea que le damos al sistema
la cual podemos esperar a que se complete o hacer otras tareas mientras esa
se completa.
Al hacer una llamada a una web a traves de internet por ejemplo a `https://dummyjson.com/products/1`
para obtener datos en formato JSON, hay una diferencia de nanosegundos en cada instrucción
que especificamos a un programa, pero por ejemplo, al hacer una solicitud web, como esta está
lejos, puede tardar un tiempo, de manera que podemos dejar que una Task haga eso y cuando tengamos
el resultado podemos usarlo, o bloquear el hilo actual para que el programa espere
a que una tarea sea completada antes de hacer cualquier otra cosa.
`Task` introduce un concepto llamado `async` y `await`, para ver eso mejor vamos a hacer
un ejemplo práctico, como obtener un producto de una supuesta base de datos que tengamos
en otro servicio.
Para eso vamos a declarar una clase que sea `Products` que tenga varios métodos estáticos
para obtener productos de esta api, que podríamos simular que fuese nuestra base de datos.
Primeramente, declararemos un método `GetProduct`, este sería `async` y devolvería un `Task<string>`.
En este caso vamos a dejar la serialización para otro supuesto servicio que no incluiremos aquí.
*/
public static class Products
{
// Los métodos `async` deben devolver un `Task` o `void`,
// Si queremos devolver un valor, deberemos usar un `Task<T>`
// donde T sea el tipo del valor a retornar, y podremos
// devolver el valor de forma normal, ya que C# va a automáticamente
// construir en una `Task` que almacene el valor y va a retornar dicha `Task`.
public static async Task<string?> GetProduct(int index)
{
// Creamos un cliente HTTP que dispose los recursos
// que usa al acabarse la ejecución de este método.
using HttpClient client = new();
// Hacemos una llamada GET a la siguiente URL, y le ponemos await,
// await significa que estamos esperando a que la tarea se complete antes de
// continuar con nuestro código, la expresión con `await` ya nos devuelve el valor
// sin una `Task`, si decidiésemos no usar `await` el tipo de la variable en este caso
// sería `Task<HttpResponseMessage>` y deberíamos esperar de otra manera a ejecutar
// código dependiente del resultado de la llamada. Para usar `await` el método debe
// ser `async`, de otra forma deberemos esperar al resultado de otra forma usando
// la api de `Task`
HttpResponseMessage response = await client.GetAsync($"https://dummyjson.com/products/{index}");
// En el caso de que la respuesta HTTP de la solicitud haya devuelto
// un código de estado que no sea 200-299 devolvemos null,
// porque eso significa que la solicitud ha fallado.
if (!response.IsSuccessStatusCode)
return null;
// Leer un string de una respuesta HTTP puede hacerse de forma
// asincrònica, usando `ReadAsStringAsync`, procedemos a devolver ese valor.
return await response.Content.ReadAsStringAsync();
}
}
/*
Ahora podemos llamar el método `GetProduct(int)` y este nos devolverá una `Task` a la que podemos
hacer await, todos los bloqueos en el hilo que hagamos dentro del método si no se hace `await`
por parte del consumidor se van a ejecutar de forma concurrente al otro código, por el contrario,
si hacemos un método async, pero ninguna llamada `await` este va a ejecutarse de forma sincrònica.
La API de Tasks es una api bastante compleja por lo que voy a dejar un par de links:
ver: https://learn.microsoft.com/es-es/dotnet/api/system.threading.tasks.task?view=net-8.0
ver: https://learn.microsoft.com/es-es/dotnet/standard/parallel-programming/task-based-asynchronous-programming.
En C# hay una API llamada `DateTime` la cual no podemos crear, la única forma que tenemos de
saber cuál es el tiempo actual sería usando esta API.
En esta API no hay nada que podamos extender, pero podemos obtener y manipular fechas.
Por ejemplo para obtener la fecha actual, `DateTime` tiene una propiedad estática
llamada `Now`, con esa podemos acceder a ese valor.
DateTime currentDate = DateTime.Now;
DateTime es un ValueType o sea un `struct` y con este podemos obtener los componentes
de la fecha como por ejemplo los segundos en un objeto, la hora del día de un objeto
y más.
Tenemos varios métodos que esta implementa como `Add`, `AddDays`, `AddSeconds`,
esta `struct` implementa `IComparable`, `IEquatable`, `IConvertible` y `IFormattable`
por lo que también tenemos la capacidad de sumar un `TimeSpan`, restarlo y demás.
currentDate += TimeSpan.FromDays(5);
Esta API nos permite deserializar muchos formatos de fecha, desde UNIX hasta ISO
y muchos formatos de las anteriormente mencionadas, por lo que podríamos
definir expiración de tokens o cualquier otra cosa de forma muy fácil.
Hablando de `DateTime` implementando `IFormattable`, `IFormattable` es
una interfaz la cual no podemos simular su comportamiento, antes hemos visto
que a un string le podemos aplicar format, siendo capaces de poner
variables dentro del string sin tener que concatenar.
Cuando hacemos $"{currentDate}" se llama al `currentDate.ToString()` y se devuelve
el resultado, pero si nosotros hacemos por ejemplo `$"{currentDate:MM/dd/yyyy}" se
va a llamar a la implementación de `ToString(string fmt, IFormatProvider provider)`
de `IFormattable` en `DateTime` y esta nos va a retornar en este caso si fuese
9 de mayo de 2024 devolvería `05/09/2024`.
Vamos a hacer un ejemplo de la implementación de IFormattable.
*/
// Creamos una parte de `Animal` con `partial` que implemente `IFormattable`
public partial class Animal : IFormattable
{
// Esta version se ejecutara si usamos `:` en el format.
public string ToString(string? format, IFormatProvider? formatProvider)
{
// en el parámetro `format` vamos a recibir lo que sea
// que el usuario de nuestra clase ponga detrás de los `:`,
// si en este caso invocamos $"{new Animal(...):hola}"
// entones el parámetro será `hola`.
// El IFormatProvider nos devuelve el proveedor de formato
// que el usuario ha pasado si es que el formato es explícito
// y no mediante un $"".
// Si el usuario tiene configuración específica de
// localización en su sistema operativo, formatProvider
// se llenará con esa.
// Devolvemos string.Empty, porque no nos interesa implementar
// nada en este ejemplo.
return string.Empty;
}
// Esta se ejecutará si de lo contrario no usamos los `:`.
public override string ToString()
=> string.Empty;
}
/*
Otra API la cual no podríamos replicar sería `Thread`, esta API nos permite
manipular hilos en el procesador.
En el caso de esta clase nos permite invocar un hilo, pararlo, obtener
su estado y más.
También podemos sincronizar los hilos con la palabra clave `lock`, compartir
variables con la palabra clave `volatile` y demás.
Para empezar vamos a hacer un ejemplo de un thread, para eso vamos a crear
una clase que se dedique a cambiar estados, vamos a simular que tenemos un
cliente y este puede cambiar la descripción de su estado.
*/
public class SocialMediaAppClient
{
private readonly string _token;
private readonly string[] _statuses = ["Hola!", "Estoy conectado!", "Que miras!?"];
// Vamos a declarar un constructor que tenga la lógica para
// conectarse a una aplicación, ya sea un bot de Discord,
// un bot de WhatsApp...
public SocialMediaAppClient(string token, string status)
{
_token = token;
SetStatus(status);
// Aquí declararemos un thread para que el status vaya cambiando.
Thread thread = new(() =>
{
// Declaramos una variable para tener en cuenta,
// por qué estado pasamos.
int iterator = 0;
// Este `while (true)` se va a ejecutar para siempre,
// pero no va a bloquear el hilo principal, ya que está
// ejecutándose en otro hilo.
while (true)
{
// iterator es igual a (¿la longitud de estados es menor o igual a iterator aumentado?)
// Si lo es entonces el valor de iterator previamente aumentado
// si no, 0.
iterator = _statuses.Length <= ++iterator ? iterator : 0;
// Llamamos SetStatus con el valor del array en la posicion [iterator]
SetStatus(_statuses[iterator]);
// Paramos el thread por 1 hora.
Thread.Sleep(TimeSpan.FromHours(1));
}
});
// El hilo que declaramos anteriormente,
// lo iniciamos, si el programa
// acaba la ejecución después de eso, el
// hilo también se va a parar.
thread.Start();
}
public void SetStatus(string status)
{
// Aquí se haría una HTTP POST request al servidor para
// cambiar el status.
}
}
/*
En cuanto a hilos también tenemos la palabra clave `lock` que nos permite
hacer que una seccion de código solo sea accedida por un solo hilo al mismo
tiempo, por ejemplo, si tenemos una variable de estado compartida entre
2 hilos la cual sirve para definir si un proceso dentro del hilo acabo
y no queremos que sea modificada por 2 hilos usamos lock.
Lock toma 1 argumento que es un objeto, este objeto creado por el hilo
y solo accesible en ese mismo, se usa como referencia a "si no tienes acceso
a este objeto no puedes acceder a esta parte del código" y muchas veces
se pasa una variable que contenga un `object`.
Vamos a dar un ejemplo
*/
public static class StatedProcess
{
// Tenemos esta variable de estado compartida
private static bool _procesoActivo;
// Objeto de bloqueo para sincronización
private static readonly object Lock = new();
// Método para realizar un proceso
public static void HacerProceso()
{
// Adquirir un bloqueo para garantizar la seguridad de la hilo
lock (Lock)
{
// Comprobar si ya hay un proceso activo
if (_procesoActivo)
return; // Salir temprano si ya hay un proceso activo
// Establecer el proceso como activo
_procesoActivo = true;
}
try
{
// Hacer un montón de cosas importantes
}
finally
{
// Asegurar que incluso si ocurre una excepción, el proceso se marque como inactivo
lock (Lock)
{
// Restablecer el proceso como inactivo
_procesoActivo = false;
}
}
}
// Método para realizar otro proceso
public static void HacerOtroProceso()
{
// Adquirir un bloqueo para garantizar la seguridad de la hilo
lock (Lock)
{
// Comprobar si ya hay un proceso activo
if (_procesoActivo)
return; // Salir temprano si ya hay un proceso activo
// Establecer el proceso como activo
_procesoActivo = true;
}
try
{
// Hacer un montón de cosas importantes
}
finally
{
// Asegurar que incluso si ocurre una excepción, el proceso se marque como inactivo
lock (Lock)
{
// Restablecer el proceso como inactivo
_procesoActivo = false;
}
}
}
}
/*
De esta forma nos aseguramos que una variable sea modificada
por solo un hilo al mismo tiempo, este patron realmente no es tan usado,
hay muchas mejores formas de hacerlo.
Y por último en cuanto a hilos tenemos variables volatiles,
para definir un campo como volátil usamos la palabra clave `volatile`.
La palabra clave volatile indica que un campo puede ser modificado por
múltiples subprocesos simultáneamente. Evita que el compilador y el hardware
realicen ciertas optimizaciones para garantizar que las lecturas y escrituras
en el campo no sean reordenadas. Sin embargo, no garantiza un orden total de
las operaciones entre todos los subprocesos
ver: https://learn.microsoft.com/es-es/dotnet/csharp/language-reference/keywords/volatile.
Una API muy extensa que tenemos en C# también es `Reflection`, esta nos permite
obtener datos de clases, métodos y demás de forma programática,
también obtener atributos de clases, invocar métodos obtenidos a traves de `Reflection`.
Vamos a empezar con la clase `Type`, esta clase contiene información de un tipo, o sea
una clase, struct...
Podemos obtener una instancia de `Type` de distintas formas, a traves de cualquier
instancia tenemos el método `GetType()` que es heredado de `object` y esta
nos devolverá el tipo, de otra manera podemos especificar el tipo que queremos
en `typeof` de esta forma
typeof(Animal) // de esta forma obtenemos el `Type` de animal.
Con `typeof` no importa si el tipo es estático, abstracto...
Ya que realmente solo obtenemos los metadatos de este, de hecho, el sí es
estático, abstracto... lo podemos obtener a traves de `reflection` programáticamente,
porque realmente `Type` es metadatos de un tipo, y este tiene esos datos en forma
de propiedades.
Dentro de `Type` también hay métodos interesantes como `GetMethod`, `GetProperty` y `GetField`
que nos permiten obtener respectivamente el método, propiedad o campo
que estemos buscando, estos 3 toman un string con el nombre del miembro y te devuelven
respectivamente un `MethodInfo`, `PropertyInfo` y `FieldInfo`
Cada uno de estos nos permite hacer cosas con el de forma programática,
pero cubrir `reflections` en su totalidad sería imposible en esta guía,
por lo que te recomiendo que vayas a Microsoft Learn y obtengas más información
sobre lo anteriormente mencionado.
Aquí te dejo unos enlaces útiles sobre `reflection`
ver: https://learn.microsoft.com/es-es/dotnet/api/system.reflection?view=net-8.0
ver: https://learn.microsoft.com/es-es/dotnet/csharp/advanced-topics/reflection-and-attributes/accessing-attributes-by-using-reflection
ver: https://learn.microsoft.com/es-es/dotnet/csharp/advanced-topics/reflection-and-attributes/attribute-tutorial
ver: https://learn.microsoft.com/es-es/dotnet/fundamentals/reflection/reflection
PROYECTOS, ENSAMBLAJES Y SOLUCIONES
Esta seccion no tiene código, es solo explicativa.
Para estructurar nuestro programa en C# usamos algo llamado solucion,
las soluciones son bibliotecas de proyectos, y cada solucion puede tener más de un proyecto.
Las soluciones nos permiten hacer flujos de despliegue de proyectos, por ejemplo
"testea el proyecto X, luego haz build a ese y despliégalo en este servidor",
en C# mayormente se hacen REST API, y se toca mucho el desplegar servidores
web, por lo que testear y desplegar nos viene muy bien.
De normal el tema de administrar soluciones y proyectos lo hace
nuestro editor, pero si queremos hacerlo en la consola lo podemos hacer de la
siguiente manera.
dotnet new sln -n TestRestApi
ver: https://learn.microsoft.com/es-es/dotnet/core/tools/dotnet-sln
De esa forma creamos una solucion, en este caso llamada TestRestApi,
si queremos añadir un proyecto a esta, por ejemplo el servicio
de REST con servidor HTTP creamos un proyecto el cual vamos a
llamar RestApi
dotnet new console -n RestApi
ver: https://learn.microsoft.com/es-es/dotnet/core/tools/dotnet-new
eso crea un proyecto en sí, hay muchas opciones que se le pueden
añadir a un proyecto como el language de programación, que puede
ser vb, f# y demás.
Cuando creemos un proyecto veremos que se ha creado una carpeta
con un csproj y varios archivos más de configuración del proyecto.
Para añadir el proyecto a nuestra solucion lo hacemos de la siguiente
manera
dotnet sln add RestApi/RestApi.csproj
Y eso lo puedes hacer las veces que sea dentro del proyecto,
con varios proyectos de distintos tipos y diferentes plantillas
ver: https://learn.microsoft.com/es-es/aspnet/web-forms/overview/deployment/web-deployment-in-the-enterprise/understanding-the-project-file
Posteriormente podemos crear un flujo de `build` del proyecto
para desplegarlo al servidor que sea.
Dependiendo de la configuración de nuestro proyecto vamos a recibir unos archivos
u otros, pero de normal siempre recibiremos un archivo `dll`
o sea un assembly.
Realmente la explicación de como funciona un proyecto en este caso es
más que nada una revision por encima porque realmente
esta guía se centra en como se programa en C#.
IMPLEMENTACIONES ÚTILES DEL LENGUAJE
En este apartado vamos a ver clases que serían reproducibles, pero no por ello dejarían
de ser útiles, como por ejemplo List<T> o Dictionary<TKey, TValue>.
A diferencia de las clases anteriormente mencionadas,
en este caso programando seriamos capaces de implementar estas clases.
Esta guía solo lleva las clases más útiles, ya que realmente hay un montón de
apis que componen dotnet, mscorlib y demás.
De las implementaciones que vamos a hablar son las siguientes
List<T>
HashSet<T>
Dictionary<TKey, TValue>
LINQ
Stream
Vamos a empezar hablando sobre listas, antes hemos visto arrays, las listas
funcionan de una forma similar, también se pueden instanciar usando `[]`,
pero la definición del tipo es con un genérico.
Realmente la definición de array es `Array<T>` y `T[]` sería su "palabra clave".
En este caso `List<T>` no tiene una palabra clave, y la diferencia que tienen
es que al contrario de un array, a esta se le puede modificar el tamaño asi
añadiendo elementos a esta, por ejemplo, podemos iniciar una lista vacía
y esta tendra un `Count` de 0.
En este caso `Count` es el remplazo de `Length` en los arrays.
Para declarar una lista lo podemos hacer de las siguientes maneras,
para el ejemplo declararé una clase estática donde poner
métodos relacionados con la manipulación de listas.
*/
public static class ListManipulation
{
// Vamos a declarar un constructor estático para tener
// un sitio donde escribir variables.
static ListManipulation()
{
// Inicializador de colección.
List<string> list = ["Hola", "Mundo"];
// Inicializador tradicional
List<string> list1 = new(){ "Hola", "Mundo" };
// Y a estas listas luego se les puede añadir elementos de forma
// programática.
list.Add("Que");
list.Add("Tal?");
// Al igual que un array normal, estos se pueden indexar.
// Los índices funcionan desde el 0, al igual que los array.
string listElement = list[0];
// Al igual que un array estas también implementan IEnumerator
// por lo que podemos iterar en ellas a traves de un foreach.
// Por cada item en la lista
foreach (string item in list)
{
// Imprimimos item en la consola.
Console.WriteLine(item);
}
// Realmente las listas no tienen mucha más diferencia
// solo que su tamaño cambia dependiendo de si añadimos
// más elementos o no.
// Para quitar un elemento de la lista lo podemos hacer de
// varias maneras
// Si tenemos elementos distintos en toda la lista podemos hacer
// lo siguiente
list.Remove("Hola");
// Si queremos quitar un índice lo podemos hacer también de 2 formas
// distintas.
// Quitamos un solo elemento en un indice
list.RemoveAt(2);
// Quitamos todos los elementos donde su carácter
// 0 sea igual a 'H'.
list.RemoveAll(item => item[0] == 'H');
// Quitamos a un index una cantidad de elementos
list.RemoveRange(2, 1);
// Usamos rangos y el operador de expansion.
list = [..list[..2], ..list[3..]];
/*
También tenemos que contar que cada vez que quitamos o añadimos
un elemento el resultado de la propiedad `Count`
va a cambiar.
Al igual que `List<T>` también tenemos `HashSet<T>`, Los HashSet no son
ordenados ni se pueden indexar, es una forma eficiente de guardar valores,
ya que el propio sistema los guarda de una forma indexados por hashes
en vez de por números que hace que sea más rápido de obtener valores.
Entonces, no podemos indexar valores con `[]` el operador de index,
pero podemos iterar por la lista, tampoco podemos esperar
que encontremos los valores en orden, esto nos sirve más en
el contexto de LINQ.
Para crear un HashSet lo hacemos igual que una lista, a diferencia
que no tenemos el `RemoveRange` ni el `RemoveAt`, solo tenemos
métodos que no estén basados en índices para manipular datos dentro
de la estructura.
*/
// Una lista de ID la cual no nos importa si está ordenada o no.
HashSet<long> idSet = [48732894723894, 4728347329084809, 894382904823904];
// añadimos un ID nuevo
idSet.Add(4829348023948);
// Por cada ID en el set
foreach (long id in idSet)
{
// imprimimos ID
Console.WriteLine(id);
}
// Quitamos un elemento que sea igual al primer argumento.
idSet.Remove(48732894723894);
/*
Luego a diferencia de los HashSet y List también podemos crear diccionarios,
estos toman 2 argumentos de tipo genérico en vez de uno, y nos permiten
indexar valores por cualquier tipo, por ejemplo un Dictionary<string, string>
nos permite hacer `var["test"]` y eso sacaria el valor indexado
en el index `"test"`, para declarar un diccionario lo haremos de la siguiente
forma.
*/
Dictionary<string, string> dictExample = new()
{
// clave = valor
["clave"] = "valor",
["clave2"] = "valor2"
};
// guardamos el valor de `"clave"` que seria `"valor"`
string value = dictExample["clave"];
// Y al igual que las List y HashSet podemos añadir valores
dictExample.Add("clave3", "valor3");
/*
Son conceptos de colecciones bastante simples donde podemos
guardar valores de formas distintas, indexar y añadir estos mismos.
Para manipular cualquiera de estas colecciones podemos usar LINQ,
de hecho, cualquier implementación de IEnumerable puede usar LINQ.
LINQ significa "language integrated query" y nos permite
hacer expresiones complejas en tunel.
En el caso de C# LINQ se basa en métodos de extension para `IEnumerable`,
antes de adentrarnos en LINQ vamos a hablar sobre predicados.
Los predicados son un tipo de funcion que te da el valor a manipular
y espera que le devuelvas el cómo manipular ese valor, por ejemplo
en un filter tomarías el elemento y si quieres que no esté en la lista
final devolverías false, previamente hablamos sobre los métodos
y lambdas, a un predicado se le puede pasar cualquier tipo de referencia
de método que sea compatible con el delegado pedido para la funcion.
Vamos a declarar una lista de personas y vamos a hacer varios ejemplos de LINQ.
*/
List<Person> persons =
[
new Person("Juan", 18),
new Person("Pepito", 50),
new Person("Jordi", 40),
new Person("Juan", 20),
new Person("Joana", 22),
new Person("Lidia", 20)
];
// Como vemos y por sentido común a una lista se le puede guardar cualquier tipo de
// objeto, como una `List<T>` también es un objeto entonces podemos guardar
// otra dentro y anidarlas, al igual que un array, podemos hacer un `array de arrays`,
// la expresión de tipo se vería tal que asi `T[][]` o `List<List<T>>`.
// Ahora que tenemos una lista de personas vamos a aplicar consultas LINQ a estas.
// Sabiendo que una vez accedemos a un elemento de una lista obtenemos una referencia
// a ese elemento en sí (si es que no es un `ValueType`), podemos acceder a las propiedades
// en los predicados.
// Vamos a por ejemplo tomar todos los usuarios que sean mayores a 25 años.
// Personas donde persona.edad sea mayor a 25
IEnumerable<Person> olderThan25 = persons.Where(person => person.Age > 25);
// Lo anterior funciona porque por cada persona está haciendo la comprobación
// de `person.Age > 25` y esa expresión devuelve un boolean, si ese boolean
// es true, nos quedamos con el valor, de otra forma, lo excluye.
// Los IEnumerable tienen métodos de extension que nos permiten
// convertirlos a `List` o a `Array` y demás con `ToArray()` o `ToList()`.
// En el ejemplo anterior nos quedamos con Pepito y Jordi, que son
// los objetos donde la propiedad `Edad` retorna un valor mayor a 25.
// También tenemos otro método llamado `Select`, este nos permite
// modificar todos los valores en una colección.
IEnumerable<string> personNames = persons.Select(person => person.Name);
// En este caso el predicado devolvería el valor con el que nos
// queremos quedar.
// También podemos ordenar los valores si es que la colección es ordenada
// como por ejemplo en el caso de listas y arrays, pero si ordenamos una
// colección sin orden este orden se va a perder, asi que no tendria sentido.
IEnumerable<Person> orderedByAge = persons.OrderBy(person => person.Age);
// En este caso el predicado quiere un número que sería el orden que ponerle,
// También tenemos el OrderByDescending.
// Los métodos LINQ se pueden concatenar porque estos retornan un `IEnumerable<T>`
// y los métodos de extension LINQ son para esa interfaz realmente.
IEnumerable<string> namesOrderedByAge = persons
.OrderBy(person => person.Age) // Ordenamos
.Select(person => person.Name); // Obtenemos los nombres de la colección ordenada.
// También tenemos el `Any` o `All` que son 2 métodos que nos permiten
// saber si algún elemento que cumpla una condicion existe o no.
// En el caso del `Any` nos devolverá true si es que al menos 1
// elemento de la colección cumple con la condicion del predicado
bool juanExists = persons.Any(person => person.Name == "Juan");
// Y en el caso de `All` nos devolverá true si todos los elementos
// cumplen con la condicion especificada en el predicado.
bool olderThanLegalAge = persons.All(person => person.Age > 18);
// Las 2 últimas se usan comúnmente en un `if`, si es que
// se cumplen para pasar por una seccion de código.
// Tenemos una version más pequeña que esa la cual es `Contains`,
// esa no es parte de LINQ y tiene una implementación
// distinta en cada tipo colección.
// En este caso `Contains` no sirve para tipos de referencia,
// ya que cuando comparas 2 tipos de referencia realmente
// estás comparando sus direcciones de memoria y no
// los valores que tengan dentro.
bool namesHasJuan = personNames.Contains("Juan");
// En este caso le pasamos un valor directamente,
// asi que deberemos evaluar en que caso nos sirve una más que la otra.
// LINQ es algo bastante extenso, te recomiendo que te mires
// alguna guía de LINQ, por aquí te dejo la oficial de microsoft learn
// ver: https://learn.microsoft.com/es-es/dotnet/csharp/linq/
/*
Por último tenemos Streams.
Los Streams son formas de leer datos que tienen un cursor integrado por
decirlo de alguna forma, el propio objeto tiene en cuenta
en que index estamos.
Los Streams de normal leen tipos simples de datos como bytes,
caracteres y asi.
Vamos a dar un ejemplo con un FileStream, que es un tipo de stream
que nos permite leer un archivo.
*/
// Este archivo estaria localizado en la carpeta bin del proyecto.
using FileStream file = File.OpenRead("./archivo.txt");
// Ahora tenemos un stream que nos permite leer por el archivo
// el cursor está localizado en la posicion 0, pero podemos
// mover ese cursor ya sea leyendo o con `Seek`.
// Vamos a leer el archivo entero.
// Creamos un buffer donde guardar los caracteres en memoria.
byte[] buffer = new byte[file.Length];
// Leemos del archivo hasta el final, ahora el cursor está en
// el final y el array de bytes que declaramos anteriormente
// está lleno.
_ = file.Read(buffer);
// Esos bytes los podemos convertir a texto con `Encoding`
// ver: https://es.wikipedia.org/wiki/Codificaci%C3%B3n_de_caracteres
string text = Encoding.UTF8.GetString(buffer);
// La variable text contiene el texto completo del archivo.
// Si queremos que el cursor vuelva al principio podemos
// hacer lo siguiente
file.Seek(0, SeekOrigin.Begin);
// Eso mueve el cursor a la posicion 0 desde el principio.
// Los streams implementan IDisposable, porque mientras
// el archivo esté abierto el programa será el único
// que puede modificarlo, si el archivo está abierto
// por el programa y lo intentamos modificar externamente nos
// aparecería el error de "El archivo está siendo usado por X programa"
// hasta que no cerráramos el archivo.
/*
Los stream tienen muchos usos, este es uno de básico,
también podemos leer recursos en red desde un stream,
y demás.
*/
}
}
/*
Hasta aquí acaba este tutorial, si has leído hasta aquí
espero que te haya servido.
Si es que es asi, estaria guay que le dieses una estrella
al GIST.
Si tienes dudas yo estoy activo en este servidor
https://discord.gg/pKMhTkRqjQ
Si pasas por ahí salúdame, me llamo `Memw`.
Gracias por leer :)
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment