En esta sección voy a indicar algunas notas a la hora de crear los métodos, básicamente los métodos que se van a utilizar son los que se muestran en la interfaz, su finalidad es crear el objeto.
Esquema de interfaces
Vamos ahora con algunas recomendaciones:
Usar parámetros solo con tipos primitivos o proporcionados por el sistema, como int, DateTime, string…, a ser posible nullables, es posible usar objetos para los métodos y en algunos casos reduce la carga de parámetros pero en ese caso estaríamos dando demasiada información sobre la creación del objeto, información que puede cambiar en un futuro y rompería la interfaz, de hecho los enumerados de usarse deberían ser propios y no los mismos que puede usar el objeto interno.
public IConnectionBuilder WithServer(
string name = "",
int? port = null,
Version? version = null)
{
Database.Server = new Server();
Database.Server.Name = name;
Database.Server.Port = port;
Database.Server.Version = version;
return this;
}
Todos los parámetros tienen valores por defecto, salvo los estrictamente obligatorios para una correcta creación del objeto que por otro lado idealmente deberían ser los mínimos. o no existir. Esto es debido a que generalmente la configuración por defecto del objeto debería ser lo más correcta posible y solo debería interesar sobrescribir algunas propiedades concretas a través de Fluent Api, acercándonos al concepto de convención sobre configuración. Otra opción es crear sobrecargas pero para objetos relativamente complejos la combinatoria de todos los parámetros a través de sobrecargas se hace demasiado complicada.
IConnectionBuilder WithServer(
string name = "",
int? port = null,
Version? version = null);
Todos los parámetros están documentados, puede parecer obvio pero en Fluent Api es fácil perder el hilo de que método se está usando y sobre que objeto está siendo aplicado así que indicar el uso y efecto es importante.
/// <summary>
/// This method sets which server will be used to connect to database.
/// </summary>
/// <param name="name">Server name or IP, by default localhost.</param>
/// <param name="port">Por to connect to server, by default 669.</param>
/// <param name="version">Driver version to use, by default none.</param>
IConnectionBuilder WithServer(
string name = "localhost",
int? port = 669,
Version? version = null);
No usar parámetros como contadores, es decir si queremos añadir 3 objetos iguales es mejor llamar tres veces al método que pasar un 3 al método, internamente a nivel de código se maneja mejor y queda más clara la intención especialmente si hay que establecer diferentes valores en estos métodos, obviamente si el valor representa internamente un contador se puede saltar esta recomendación.
Una nota en particular para los métodos de extensión en C#, una ventaja que tienen es que permite crear métodos que no existan en la interfaz lo que permite esconder algunos métodos que no queremos mostrar en la interfaz o darles una implementación concreta (sí, con C# 8 se puede hacer lo mismo con la implementación de código por defecto en interfaces pero en este caso no queremos ni mostrar el método en la interfaz), de hecho se podría crear Fluent Api solo con métodos de extensión indicando en Fluent Api simplemente la herencia, parecido a como sería con mixin. Aunque puede parecer más cómodo no resulta muy legible ni queda claro el propósito de cada interfaz, por otro lado al ser métodos de extensión solo se puede trabajar sobre objetos estáticos y eso puede complicar el código. El ejemplo propuesto tiene dos interfaces para dos propósitos, configurar conexiones y configurar usuarios, imaginemos que no queremos usar interfaces y solo usar una para configurar por ejemplo los usuarios y dejar la conexión en su configuración por defecto, este sería un ejemplo donde un método de extensión puede ser útil, de cara a la interfaz siguiendo con el patrón solo existe un método sin contexto que está en la interfaz principal IDatabaseBuilder.Save() pero vía extensión hay un método Save() para cada interfaz.
public static string Save(this IUsersBuilder usersBuilder)
{
JsonSerializerSettings settings = new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.All };
return JsonConvert.SerializeObject((usersBuilder as DatabaseBuilder).Database, settings);
}
Aunque es posible implementar Fluent Api con clases realmente la limitación de algunos lenguajes con la herencia múltiple hace que la costumbre sea usar interfaces, de hecho resulta más útil ya que es posible crear diferentes implementaciones especialmente en sistemas de configuración.
Diagrama interfaces
Lo más relevante de cara a las interfaces es:
El nombre indica que contiene, esto quiere decir que no importa mucho que el nombre sea un poco largo si indica que métodos va a incluir o que se puede hacer con el, hay que tener en cuenta que el nombre de la interfaz generalmente no será visible en código. Tampoco es inusual que el nombre interfaz se componga como una concatenación de todos las interfaces padres lo que a la hora de buscar o entender el código resulta más cómodo a falta de un diagrama a mano.
Crear interfaces hija al añadir objetos, si el objeto interno tiene una lista y requiere añadir objetos a esa lista lo recomendable es crear una nueva interfaz para construir objetos de esa lista interna, y el método AddXX correspondiente devolverá dicha interfaz hija para que pueda ser usada para configurar ese elemento hijo, de esta forma se proporciona métodos específicos para este objeto que no pueden ser usados por otros en las interfaces superiores. En el siguiente ejemplo se agrega un usuario y se devuelve una ICredentialBuilder que es específica para crear las credenciales de un usuario.
public ICredentialBuilder AddUser()
{
login = new Login();
logins.Add(login);
group.Logins.Add(login);
return this;
}
Las interfaces hija siempre heredan directa o indirectamente de la principal, de lo contrario nos quedaríamos bloqueados y no podríamos acceder a los métodos de la interfaz principal para continuar creando el objeto y finalmente guardarlo a través del método sin contexto. En el siguiente ejemplo IUserBuilder hereda de IGroupBuilder para poder añadir usuarios a un grupo, es decir que directamente no hereda de la interfaz principal IUsersBuilder pero si a través de IGroupBuilder.
public interface IUsersBuilder
{
...
}
public interface IUserBuilder : IGroupBuilder
{
ICredentialBuilder AddUser();
}
public interface IGroupBuilder : IUsersBuilder
{
IUserBuilder AddGroup(
string name = "");
}
Solo la interfaz principal debe tener un método void, como indica la propia definición de Fluent Api en algún momento debemos poder indicar que el objeto está terminado y se puede construir, esté método debería estar solo en la interfaz principal para evitar crear objetos incompletos, ejemplos clásicos son Build(), Save(),… Está bien, es verdad que se puede usar un método de extensión para sortear esta limitación, por ejemplo guardar resultados parciales, pero a no ser que sea necesario no es lo recomendable.
public interface IDatabaseBuilder : IConnectionBuilder, IUsersBuilder
{
string Save();
}
Usar el mismo fichero para crearlas, si no son muchas es más cómodo tenerlas todas juntas en un fichero que en varios separados, sobre todo porque generalmente son interfaces con pocos métodos y al haber una relación jerárquica entre ellas vía herencia es más cómodo verlas todas juntas, como nombre de fichero lo más cómodo es poner el nombre de la interfaz principal que suele ser la que mejor define el uso.
Así como usar métodos AddXXX indica que la interfaz que devuelve servirá para crear objetos de una lista interna, es posible tener mayores niveles de anidación, por ejemplo listas de listas, esto se puede solucionar creando una interfaz que represente esta relación de creación de listas, dentro de la implementación se hará la correcta creación de las mismas. En el ejemplo el método AddGroups() representa este efecto, este método devuelve otra interfaz IGroupBuilder encargada de crear la lista anidada y a su vez está interfaz tiene el método que añade elementos individuales AddGroup().
public interface IUsersBuilder
{
ICredentialBuilder AddLogin();
IGroupBuilder AddGroups();
}
public interface IGroupBuilder : IUsersBuilder
{
IUserBuilder AddGroup(
string name = "");
}
public interface IUserBuilder : IGroupBuilder
{
ICredentialBuilder AddUser();
}
Puede ser que tengamos dos interfaces cada una de las cuales se encargue de una parte concreta de la creación del objeto, en este ejemplo hay una interfaz para crear conexiones IConnectionBuilder y otra para crear usuarios IUsersBuilder. Si necesitásemos usar ambas necesitamos una interfaz que nos las conecte, ese es el propósito de la interfaz IDatabaseBuilder. Mantenemos una referencia a la interfaz principal y la usamos para configurar cada una de las partes concretas del objeto.
Detrás de una gran interfaz siempre hay una gran clase, y en este caso no es una excepción, en general si la interfaz está bien definida en términos de herencia y parámetros de devolución no debería haber problemas.
Diagrama clases
Algunas recomendaciones:
El objeto interno y en general todos los que se usen deben de ser privados, esto es obvio si permitiésemos que métodos externos modifiquen el objeto interno no tendría sentido usar Fluent Api, hay que revisar todos los modificadores de acceso a todos los objetos que se usen dentro de la clase.
Solo puede haber un objeto interno raíz, es cierto que puede interesar tener una colección pero en ese caso es mejor tener un objeto que albergue una colección, tener un solo elemento raíz simplifica el desarrollo, por lo que en caso de querer tener una colección sería mejor crear un objeto raíz que englobe esta colección.
public class DatabaseBuilder :
IDatabaseBuilder,
IUserBuilder,
IRoleBuilder,
IRightBuilder,
IGroupBuilder,
ICredentialBuilder
{
...
}
Los elementos no persisten hasta llamar al método sin contexto, cualquier uso de métodos modificará el objeto en memoría y no estará disponible a través de ningún método de acceso hasta que se llame al método sin contexto (Save(), Build()…) que es el que realmente creará el objeto o lo almacenará, si fuera acceso a una base de datos el commit iría en este punto, sino, es posible emular este efecto creando objetos o listas temporales. En el ejemplo de muestra se usan variables privadas que almacenan partes del objeto que se usan en varias partes del método.
internal readonly Database Database = null;
private List<Right> rights = null;
private List<Login> logins = null;
private List<Group> groups = null;
private Role role = null;
private Login login = null;
private Group group = null;
Crear varios ficheros, en este caso daría el consejo contrario a las interfaces, a no ser que sea poco código si hay varias interfaces es recomendable tener las implementaciones en ficheros diferentes para evitar mezclarlas y que se vea más claro el uso de cada clase.
Vigilar la inicialización y construcción de objetos, usar Fluent Api no debería ser complicado y una buena inicialización de objetos en las clases superiores permite que estén disponibles para métodos anidados y no tengan que preocuparse de una creación condicional. Si el código está correctamente relacionado llamar al método final sin contexto debería ser trivial. Por ejemplo el método AddGroup crea no solo la lista de posibles grupos anidados sino también la lista de logins ya que al llamar a este método la única opción es llamar a métodos de la interfaz IUserBuilder que va a usar estos objetos.
public IUserBuilder AddGroup(string name = "")
{
groups = new List<Group>();
group = new Group();
group.Name = name;
logins = new List<Login>();
group.Logins = logins;
groups.Add(group);
Database.Groups.Add(groups);
return this;
}
No usar constructores públicos, es mejor tener un método de factoría que devuelva la interfaz principal y sea esta la que se use durante la configuración, generalmente la creación del objeto por defecto suele ser necesaria y estos detalles deberían quedar ocultos. En el ejemplo se usar un método estático que crea el objeto interno y devuelve la interfaz principal para poder continuar configurándolo.
private static IDatabaseBuilder databaseBuilder = null;
public static IDatabaseBuilder Create()
{
databaseBuilder = new DatabaseBuilder();
return databaseBuilder;
}
private DatabaseBuilder()
{
Database = new Database();
Database.Users = new List<Login>();
}
Dentro de una misma solución de Visual Studio podemos ver los comentarios sin problema, sin embargo cuando creamos una librería por ejemplo a través de NuGet también nos interesa que estos comentarios sean visibles para el consumidor de la librería, esto es posible gracias a unos ficheros XML que incluyen estos comentarios.
Estos ficheros XML no se generan de forma automática, para habilitarlos tenemos dos opciones, a través de la interfaz de Visual Studio o modificando el archivo del proyecto (.csproj).
SDK (archivo de proyecto)
La opción más segura es cambiando el fichero de proyecto usnado la etiqueta <GenerateDocumentationFile>, esto generará los ficheros XML en la ruta adecuada para el proceso de creación del paquete (dotnet pack, nuget pack,…)
<Project>
<PropertyGroup>
<!-- Allows to see coments in code -->
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- Optional directory path, only if necessary -->
<DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
</PropertyGroup>
</Project>
Esto puede ser hecho de forma global usando un fichero Directory.Build.props colocado en la raíz de la solución, por ejemplo añadiendo el contenido del ejemplo anterior todos los proyectos de la solución generarán la documentación.
<DocumentationFile> indica la ruta donde se colocarán los ficheros XML, pero como se ha comentado antes es mejor no usarla.
Visual Studio
En este caso usamos la interfaz, simplemente vamos a las propiedades del proyecto a la sección Build y marcamos XML documentation file, que equivale al tag <GenerateDocumentationFile> esto automaticamente rellenará el campo con una ruta por defecto que equivale al campo <DocumentationFile> .
Hay que tener en cuenta que esta configuración es para Debug así que para otras configuraciones habrá que hacer lo mismo.
También es recomendable habilitar los analizadores de código para que muestren avisos de todos los métodos públicos que tendrían que estar documentados.
Sandcastle
SandCastle es una herramienta desarrollada por Microsoft que genera documentación de forma automatizada en un formato similar al que podemos ver en la propia documentación de referencia oficial de Microsoft, Sandcastle se puede descargar desde: https://github.com/EWSoftware/SHFB
En la sección Build seleccionamos la versión de framework que vamos a usar y la carpeta donde guardaremos el log, es útil colocarlo en la misma carpeta que el archivo de proyecto pero no es necesario asociarlo al código fuente, hay que indicar en este caso Fixed Path para que no lo ubique en la ruta por defecto sino en la indicada.
Indicamos también en que formato queremos mostrar la documentación, según como queramos publicar la documentación elegiremos uno u otro (web, html help,…).
En el panel Project Explorer dentro de Documentation Sources sacamos el menú contextual y agregamos los archivos xml de documentación y los ensamblados que harán falta posteriormente para que Sandcastle pueda usar reflection de cara a generar por ejemplo la información de los namespaces que se verá más adelante.
Dentro de References podemos agregar otros proyectos o ensamblados a los que se haga referencia pero de los cuales no se quiera generar documentación.
Agregar otros proyectos
En la sección Help File es donde indicaremos información relativa al archivo de documentación. Indicamos los campos Help Title que es el título para la documentación, el nombre del archivo generado en Help file name y el idioma en Help file language. Podemos indicar campos opcionales como Feedback e-mail address para indicar quien es el autor de la documentación, así como un texto asociado a esta dirección en Feedback e-mail link text.
Información del archivo de documentación
En la sección Summaries vamos a indicar un texto para los namespaces ya que Visual Studio por defecto no permite comentarlos y Sandcastle genera mensajes de advertencia. Usando el botón Edit namespace summaries podemos seleccionar el namespace que queramos comentar junto con un texto asociado.
Documentación para namespaces
La documentación generada se ubicará en la carpeta help dentro de la carpeta que hayamos especificado.
En la salida (pestaña Build Output) se pueden ver los errores generados, habitualmente enlaces incorrectos.
Simplemente usando el comentario triple /// en la cabecera de una estructura de código (clase, método,…) automáticamente se generará la base del comentario, como dato adicional los namespaces no se pueden comentar.
Estos comentarios que generemos se guardarán en ficheros XML, por lo tanto al comentar hay que tener en cuenta que algunos caracteres pueden dar error, por ejemplo ‘<’ se convierte en < y ‘>’ se convierte en >
También comentar que algunas de las etiquetas que se van a explicar pueden no mostrarse correctamente a través de IntelliSense pero si se verán correctamente en la documentación final.
Uso de enlaces
Cualquier etiqueta de documentación es susceptible de usar el atributo cref, este atributo tiene ciertas características para hacer referencia a otros elementos de código como se verá más adelante, por ejemplo para agregar una referencia a un tipo genérico usaríamos:
<see cref="Robot{T}" />
La lista a continuación son prefijos que se pueden agregar para especificar el tipo de código al que se hace referencia, si está bien referenciados no hace falta ya que el compilador lo agrega automáticamente, dependiendo del tipo de enlace se usará un prefijo u otro:
A continuación la referencia de las etiquetas disponibles, todas las etiquetas se explican con la misma estructura:
Nombre de la etiqueta.
Descripción.
Ejemplo de sintaxis.
Imagen de ejemplo del resultado en documentación en línea creada con SandCastle (muy similar a MSDN) y en Visual Studio si aplica.
<summary>
Describe un tipo o miembro del mismo. Este texto es el único origen de información para IntelliSense, y se muestra también en Object Browser Window. Admite también el atributo cref. Es el elemento por defecto que aparece al empezar la documentación con ///
/// <summary>
/// Base constructor por person object.
/// </summary>
<c> <code>
El texto indicado representa código y se representará como tal, con el estilo de letra para código, si se usa <code> aparecerá encuadrado. Ejemplo:
/// <summary>
/// Name used for <c>Person</c> object.
/// It can be used as <code> Person p = new Person(); </code>
/// </summary>
<example>
Crea un ejemplo para la documentación, si es para crear un ejemplo de código es recomendable usarlo junto con la etiqueta <code>.
/// <example>
/// It defines a SecondName for the user,
/// it is used normally with <c>Name</c> as
/// <code>string s = Name + SecondName;</code>
/// </example>
<exception>
Sirve para indicar que excepciones se lanzarán, se puede usar con métodos, propiedades, eventos e índices. Se puede usar el atributo cref que permite indicar una referencia a una excepción.
/// <exception cref="ArgumentNullException">
/// Returns error if <c>Name</c> is null.
/// </exception>
<include>
Permite incluir comentarios adicionales en otro archivo, esto es útil para poder usar varios ficheros a la vez y no incluir los comentarios directamente en código.
Se usan los siguientes atributos: file con el nombre del fichero adicional y path con la ruta XPath al comentario de código.
Al compilar el contenido el fichero referenciado se incrusta en la sección que hace la referencia como si se hubiera escrito directamente.
A través de XPath accedemos a la estructura xml que contiene la documentación propiamente dicha en el formato esperado. Un ejemplo de sintaxis es:
Permite crear una lista, el elemento <list> admite varios valores bullet/number/table cada uno de los cuales genera la lista en un formato diferente, no numerada, numerada o en formato tabla. Solo si se usa la opción table tiene sentido usar la etiqueta listheader, term y description se pueden considerar como una etiqueta del estilo clave/valor.
/// <summary>
/// It defines a person completely.
/// <list type="table">
/// <listheader>
/// <term>Person</term>
/// <description>Different people.</description>
/// </listheader>
/// <item>
/// <term>Normal.</term>
/// <description>Person from this planet.</description>
/// </item>
/// </list>
/// </summary>
<para>
Se usa dentro de otras etiquetas como <summary> <remarks> <returns> para agregar estructura al código, como crear párrafos.
/// <summary>
/// Duplicates the name.
/// <para>Generally it is not useful,
/// but for any person people love it.</para>
/// </summary>
<param>
Sirve para agregar información al parámetro de una función.
/// <param name="secondName">
/// The second name to append.
/// </param>
<paramref>
El nombre de parámetro al que se quiere hacer referencia, es útil para indicar en etiquetas <summary> o <remarks>, por defecto en Sandcastle aparece en cursiva.
/// Clones a person with the
/// <paramref name="name">supplied
/// </paramref>.
<typeparam>
Sirve para comentar un tipo genérico o describir un parámetro de tipo. Se agrega una etiqueta para cada parámetro de tipo. El texto se mostrará en IntelliSense, y en Object Browser Window.
/// <typeparam name="T">
/// Type for the array, must be <c>Person</c>
/// </typeparam>
<permission>
Útil para documentar el acceso a un miembro, hay que indicar la clase que gestiona el acceso, para esto se usa habitualmente la clase PermissionSet.
/// <permission cref="System.Security.PermissionSet">
/// Everyone can access this method.
/// </permission
<remarks>
Se usa para agregar información adicional sobre un tipo, proporcionando más información que la suministrada por <summary>. Esta información se muestra en Object Browser Window.
/// <remarks>
/// This is a critical class due to his abstract aspect.
/// </remarks>
<returns>
Representa el valor devuelto por la función.
/// <returns>
/// Name concatenated itself.
/// </returns>
<see>
Permite especificar un enlace dentro de un texto, el atributo cref permite crear hipervínculos a páginas de documentación.
/// <see
/// cref="System.Console.WriteLine(System.String)"/>
/// to write to console.
<seealso>
Para indicar el texto que se mostrará en la sección See Also.
Características adicionales que no encajan en otros apartados
Configuración de tipos de referencia Nullable
Son una característica nueva que permite una mayor gestión a la hora de decirle al compilador cuando una variable es nula o no.
Esto es muy útil a la hora de crear librerías para mostrar advertencias a los usuarios sobre posibles efectos en el uso de las funciones en el manejo de valores nulos.
Esta característica está pensada para .Net Core 3 y .Net Standard 2.1. Si hay algún problema se puede configurar explicitamente la versión del lenguaje a usar.
A partir de aquí hay dos opciones para habilitar esta característica. La primera está pensada para proyectos que ya están en marcha, así que lo que se hace es habilitarlo a nivel de proyecto con:
Y luego deshabilitarlo en cada fichero incluyendo:
#nullable disable
Esto generará avisos por el código, una vez que se han corregido todos se puede eliminar la directiva en cada fichero.
La segunda opción es la inversa, es decir, habilitarlo en ficheros individuales:
#nullable enable
Esto se hace en cada fichero hasta que todos estén anotados y las advertencias del compilador corregidas y entonces se habilita en el proyecto y se eliminan de los ficheros:
En un contexto nullable como el que se ha habilitado hay que tener en cuenta:
Cualquier variable por referencia es una referencia no nullable.
Caulquier referencia no nullable puede ser desreferenciada de una forma segura.
Un tipo de referencia nullable (usando ? como string?) puede ser nula. Durante un análisis estático se verifica si el tipo puede ser nulo de cara a mostrar o no una advertencia.
Se puede usar el operador ! para indicar que una referencia no será nula (se verá más adelante).
Tipos de referencia Nullable
Ahora partimos de la siguiente interfaz:
namespace UpToDate.Helpers
{
public interface ISer<in T, out U>
where T : notnull
where U : notnull
{
U MagicCast(T input);
}
}
Hemos indicado que tanto T como U no pueden ser nulos, por lo tanto solo se aceptarán valores no nulos o tipo valor (string?, int? darían error).
Se puede usar también la restricción class como se ve en el ejemplo pero en este caso solo podría ser un tipo referencia y no valor.
Esto también obliga a que cualquier clase que lo implemente debe incorporar las mismas restricciones para evitar los avisos del compilador como se pueden ver en las restricciones añadiendo notnull.
public class Sorcerer<T, U> : ISchool<T, U>
where T : notnull
where U : class
{
private string name = string.Empty;
[AllowNull]
public string Name
{
get
{
return name;
}
set
{
name = value ?? string.Empty;
}
}
public U Cast(T input)
{
return (input as U)!;
}
}
Por lo que esta línea no funcionaría:
return new Sorcerer<string?, float?>();
Pero esta sería correcta:
return new Sorcerer<int, float>();
Nullable preconditions
Algunos atributos son precondiciones, esto quiere decir que solo se aplican a valores de entrada por ejemplo en una propiedad solo se aplicaría a set.
Siguiendo con este ejemplo podemos querer que ciertos valores acepten valores null, para eso es el atributo [AllowNull] de la propiedad Name, el ejemplo anterior evita que la siguiente línea de código de una advertencia:
var sorcerer = new Sorcerer<float, string>();
sorcerer.Name = null;
También se puede trabajar en la dirección contraria, impidiendo asignar valores nulos y emitiendo advertencias, por ejemplo si tenemos este código:
Person? p = null;
sorcerer.LowerName(ref p);
Indicar esa firma de método con [DisallowNull] provocará que el compilador emita una advertencia, hemos convertido la referencia a referencia nullable (usando ?) y después con el atributo impedimos que sea nula, ya que en contextos nullables los tipos referencia se consideran seguros para dereferenciar.
public string LowerName([DisallowNull]ref Person? person)
{
...
}
Nullable postconditions
También se pueden indicar posibles valores en la devolución, por ejemplo se puede indicar que una función puede devolver un valor nulo con [return: MaybeNull], esto es habitual en genéricos sin restricciones como también se verá más adelante, por ejemplo:
[return: MaybeNull]
public Z Find<Z>(Z s)
{
Z z = default(Z);
return s!.GetHashCode() > 100 ? s : z;
}
Puede devolver un nulo y por eso lo marcamos como [return: MaybeNull] de tal forma que el código que haga uso de este método emitirá una advertencia, como el siguiente:
public int ReturnNotNull()
{
var sorcerer = new Sorcerer<float, string>();
string z = "4";
var x = sorcerer.Find(z);
return x.Length;
}
O también podemos indicar que si el valor devuelto es por referencia nunca será nulo usando [NotNull], por ejemplo para este método se está indicando que el parámetro z que se envía por referencia a la función nunca será devuelto como nulo, aunque pueda ser enviado como nulo a la función.
Es decir, independientemente del valor introducido siempre devolvereremos un valor no nulo.
public void Swap<Z>([NotNull]ref Z[]? z)
where Z : new()
{
if (z is null)
{
z = new Z[] { new Z() };
}
}
Y podríamos usarlo de forma segura sin avisos ya que hemos indicado que no será nulo:
Person?[] people = null;
extra.Swap(ref people);
Console.WriteLine(people.Length);
Tanto [NotNull] como [MaybeNull] pueden ser usados en valores devueltos en cualquier forma: in, ref, out o return.
Conditional postconditions
Puede ser que queramos indicar si el valor devuelto será nulo o no en función del parámetro de devolución de la función, por ejemplo si la función devuelve true queremos indicar que el parámetro devuelto no será nulo, es una estructura muy típica de las funciones estilo TryXXX(out x).
Por ejemplo queremos indicar que el parámetro devuelto no será nulo cuando la función devuelva false.
public bool IsNullOrEmpty([NotNullWhen(false)] string? value)
{
return String.IsNullOrEmpty(value);
}
Y lo usaríamos de la siguiente forma para obtener una advertencia al saber que la variable será nula. El compilador sabe que esto es un riesgo porque se ha establecido que no será nulo cuando el valor devuelto sea false.
Incluso existe el caso de que el parámetro pueda ser nulo aunque el propio tipo no lo permita para eso tenemos [MaybeNullWhen(bool)] , por ejemplo creamos una cola personalizada:
public class CustomQueue<T>
{
private readonly Queue<T> queue = new Queue<T>();
public bool TryDequeue([MaybeNullWhen(false)] out T result)
{
result = queue.Dequeue();
return result != null;
}
}
Entonces una línea como esta mostraría una advertencia ya que intentamos acceder a una variable que sabemos que puede ser nula.
Esto es muy habitual en genéricos donde los tipos pueden ser indistintamente referencia o valor y no hay forma de saber por adelantado si el valor podrá ser nulo o no.
CustomQueue<string> queue = new CustomQueue<string>();
if (!queue.TryDequeue(out var result))
Console.WriteLine(result.Length);
Dependencias de nulidad entre entrada y salida
Podría ser que quisieramos que un valor de salida fuera nulo en función del valor de entrada, si por ejemplo la entrada no es nula la salida del método tampoco lo será, esto lo hacemos con [return: NotNullIfNotNull(param)]
[return: NotNullIfNotNull("s")]
public string? ToLower(string? s)
{
if (String.IsNullOrEmpty(s))
return string.Empty;
return s.ToLower();
}
O en C# 11 se puede usar nameof para poder referenciar parámetros genéricos
[return: NotNullWhen(nameof(value))]
public T Process<T>(T value, bool condition)
{
if (condition)
{
return value;
}
return default(T);
}
Este código lanzaría una advertencia ya que estamos pasando un valor nulo, este valor en este caso viene dado de forma explícita pero podría venir dado desde un parámetro de otra función.
string? toLower = null;
var lower = extra.ToLower(toLower);
Console.WriteLine(lower.Length);
Atributos de flujo
Podría ser que durante el flujo de la ejecución haya un método que haga una comprobación como:
Emitir una excepción si el parámetro por ejemplo es nulo.
Una función del tipo assert que emitirá una excepción dependiendo de un booleano de entrada, por ejemplo un true lanzaría una excepción pero un false no.
[DoesNotReturn]
public void ThrowLowerException(string s)
{
throw new ArgumentException(s);
}
public void AssertIsLower([DoesNotReturnIf(false)] bool s)
{
if (s)
throw new ArgumentException("Lower string");
}
En el primer caso indicamos que no hace falta verificar el valor de retorno ya que será una excepción, y en el segundo indicamos que no hace falta realizar ninguna comprobación si el parámetro de entrada es false ya que la ejecución del código terminará en ese punto.
Null forgiveness operator (!)
Habíamos visto un operador ! este operador sirve para indicar que el parámetro no es nulo y no lo va a ser, es una forma de evitar algunas advertencias si sabemos de antemano que ese valor no puede ser nulo.
Se suele usar en pasos intermedio mientras la librería se prepara siguiendo los pasos anteriores, pero a medida que se normalize usando los atributos este operador debería ir desapareciendo.
Cuando se crea una aplicación hay que añadir una cierta cantidad de código que algunas veces no es necesario, ahora se puede evitar y simplificar, por ejemplo, un código clásico como:
using System;
namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}
Se quedaría en algo como:
using System;
Console.WriteLine("Hello World!");
System.Console.WriteLine("Hello World!"); // More succintly
Esto solo se puede hacer para un único fichero, así que es especialmente útil para aplicaciones de consola (mantiene el uso de args[] y se pueden devolver valores) o pequeños scripts como los que se pueden ver en Azure Functions o Jupyter Notebook.
Cualquier novedad interesante para tratar con funciones.
Información de contexto
Es posible obtener información de contexto sobre la llamada a una función.
public void WriteMessage(string messageBefore,
[System.Runtime.CompilerServices.CallerMemberName] string memberName = "",
[System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "",
[System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{// C# 5
Console.WriteLine("parameter before: " + messageBefore);
Console.WriteLine("caller: " + memberName);
Console.WriteLine("source file path: " + sourceFilePath);
Console.WriteLine("source line number: " + sourceLineNumber);
Console.WriteLine("parameter after: " + messageAfter);
}
/* El resultado es:
parameter: Example
member name: Main
source file path: C:\repos\UpToDate\src\UpToDate\Program.cs
source line number: 28
*/
Existe también la opción de indicar un parámetro que el compilador reemplaza con la representación de texto de otro argumento, en este caso si no se cumple la validación (condition = false) en el mensaje se pondrá como texto el nombre de la variable que se usó como parámetro validateSomethingInteresting
Las propiedades aceptan inicializadores, código que asigna un valor inicial, este código tiene que ser estático (una constante, una función estática,…)
// Llamando a una función estática
public string Surname { get; set; } = ReturnDiscard(5).ToString();
// O a una constante
public string Surname { get; set; } = "Surname".ToLower();
Declaración y uso de parámetros out
Es posible devolver valores por referencia
public ref string ReturnByReference(int number, string[] names)
{
return ref names[number]; // return the storage location, not the value
}
// Uso
string[] dummies = new string[] { "John", "Doe" };
ref var refString = ref functions.ReturnByReference(1, dummies);
refString = "surname"; // Esto modifica el elemento 1 del parámetro dummies
System.Console.WriteLine(dummies[1]);
/* El resultado es:
surname
*/
También es posible que este no sea el comportamiento deseado y no se quiera poder modificar el valor retornado, se puede usar readonly para esto:
public ref readonly string ReturnByReferenceReadonly(int number, string[] names) // 'readonly' es la palabra clave
{
return ref names[number];
}
// Uso
string[] dummies = new string[] { "John", "Doe" };
var refString = functions.ReturnByReferenceReadonly(1, dummies);
refString = "surname"; //Esto no modificará el elemento 1
System.Console.WriteLine(dummies[1]);
/* El resultado es:
Doe
*/
Funciones locales
Es posible crear funciones locales, es decir funciones que existen dentro de otras.
// Función local auto-implementada
public int LocalFunctionsAuto(int a, int b)
{
int Sum(int a1, int b1) => a1 + b1;
return Sum(a, b);
}
// Función local
public int LocalFunctionsRegular(int a, int b)
{
int Sum(int a1, int b1) {
return a1 + b1;
}
return Sum(a, b);
}
Parámetro in
Un parámetro in no se puede modificar por la función que lo usa, por lo tanto tiene que ser inicializado antes de ser enviado a la función, digamos que es un readonly para la función que lo usa.
public void InParameter(in int o)// Because of 'in' o can not be modified
{
//o = 3; Esto provocaría un error CS8331
Console.WriteLine(o);
}
// Uso
functions.InParameter(66);
/* El resultado es:
66
*/
Devolviendo múltiples valores
Se pueden devolver varios valores desde una función, la función llamadora recibirá un objeto de la clase Tuple.
public (int, double) Average(int[] values)
{
return (values.Length, values.Average());
}
// Uso
var items = Average(new int[] { 1, 2 });
Console.WriteLine($"For {items.Item1} elements the average is {items.Item2}");
/* El resultado es:
For 2 elements the average is 1.5
*/
Deconstructores
Se puede deconstruir un objeto y devolver una tupla de elementos, para esto hace falta una función especial llamada Deconstruct si se quiere por ejemplo extraer dos valores esta función aceptará dos parámetros.
// Esta función establecerá los valores a devolver
public void Deconstruct(out string firstName, out string lastName)
{// It can be also an extension method
firstName = FirstName.ToLower();
lastName = LastName.ToLower();
}
// Uso
DataStructures dataStructures2 = new DataStructures("John", "Doe");
var (f, l) = dataStructures2;
System.Console.WriteLine(f); // => FirstName.ToLower()
System.Console.WriteLine(l); // => LastName.ToLower()
/* El resultado es:
john
doe
*/
De igual forma se pueden crear métodos Deconstruct con más parámetros que asignarán más valores a variables.
[C#10] No hace falta que se asignen todos los parámetros, por ejemplo:
int x = 0;
(x, int y) = point;
Private protected
Nuevo modificador de accesibilidad que permite que un método sea llamado tanto desde la propia clase como desde una clase que herede de esta siempre y cuando estén en el mismo ensamblado.
private protected string Example()
{// Este método solo puede ser llamado por un padre en el mismo ensamblado
return "Private protected";
}
Funciones locales estáticas
Ahora es posible crear funciones locales estáticas, esto impide hacer uso de las variables del ámbito padre al contrario que las funciones no estáticas, de lo contrario se generaría un error.
public int Father()
{
int a = 1;
int b = 2;
return Son(a, b);
static int Son(int c, int d) => c + d;
}
Inicializador de módulos (C# 9)
Ahora se pueden inicializar módulos a través de funciones marcadas con el atributo [ModuleInitializer] estas funciones se ejecutarán antes que cualquier otro método en el módulo, por ejemplo este código será el primero que se ejecute antes que cualquier otro.
[ModuleInitializer]
public static void Start()
{
System.Console.WriteLine("I'm the first one");
}
Por ejemplo novedades en tuplas, interfaces, structs…
Tuplas
Se pueden crear tuplas directamente, una tupla almacena una colección de valores, no confundir con el tipo Tuple (que es una clase y por lo tanto tipo referencia) este tipo realmente es ValueTuple y es una estructura (paso por valor).
Se pueden establecer nombres a los elementos o declararlos de forma anónima.
string a = "A";
string b = "B";
var tuple = (a, b); // Tupla si nombre
System.Console.WriteLine(tuple.a); // Se usa el nombre de la variable
System.Console.WriteLine(tuple.b);
/* El resultado es:
B
*/
var tupleNamed = (key: a, value: b); // Tupla con nombre
System.Console.WriteLine(tupleNamed.key);
System.Console.WriteLine(tupleNamed.value);
/* El resultado es
A
B
*/
Las tuplas tienen características especiales, por ejemplo es posible compararlas, la comparación es por estructura y valor, es decir mismos elementos y mismo valor.
Los elementos pueden estar en otra ubicación si tienen nombre asignado y se pueden relacionar sino será una comparación posicional.
var tuple1 = (key: "1", value: "1.1");
var tuple2 = (key: "1", value: "1.1");
Console.WriteLine(tuple1 == tuple2 ? "Equals" : "Not Equals");
/* El resultado es
Equals
*/
Structs
Un struct puede ser de tipo lectura, en ese caso no se puede modificar ningún valor del mismo y por lo tanto debería ser inicializado por completo dentro del propio struct, por ejemplo en el constructor.
public readonly struct Person
{
public string Name { get; }
public string Surname { get; }
public Person(string name, string surName)
{
this.Name = name;
this.Surname = surName;
}
}
// Uso
DataStructures.Person structure = new DataStructures.Person("John", "Doe");
Console.WriteLine(structure.Name);
Console.WriteLine(structure.Surname);
/* El resultado es
John
Doe
*/
También es posible marcar más elementos como readonly esto obliga a que si un método es marcado como readonly no se pueden asignar valores dentro del mismo.
public struct Animal
{
public string Genre { get; set; }
public string Specie { get; set; }
public Animal(string genre, string specie)
{
this.Genre = genre;
this.Specie = specie;
}
public readonly string FullString()
{
// Specie = "EE"; Esta línea produciría un error.
return $"{Genre} -> {Specie}";
}
}
También hay que tener en cuenta que usar elementos no marcados como readonly dentro de métodos marcados como readonly generan una copia del mismo lo cual puede producir una penalización en el rendimiento.
[C#10] Se pueden crear structs sin tener que crear constructores con todos los parámetros. Esta inicialización se puede combinar, inicializando algunos valores en el constructor y otros directamente en las propiedades. Se pueden incluso crear arrays que se inicializarán a los valores por defecto de la estructura.
public readonly struct PersonStruct
{
public string Name { get; }
public string Surname { get; } = "Goodbye";
public PersonStruct()
{
Name = "Hello";
}
public PersonStruct(string name, string surName)
{
this.Name = name;
this.Surname = surName;
}
}
var ps = new PersonStruct[2];
Console.WriteLine(string.Join(", ", ps)); // Shows (), ()
Otra forma de inicializar es no crear ningún constructor, declarar la variable e inicializar los valores antes de usarla.
public struct Address
{
public string Street;
public int Number;
public double Distance;
public override string ToString() => $"{Street} ({Number}) in {Distance}";
}
Address address;
address.Street = "Street";
address.Number = 66;
address.Distance = 100.9;
Console.WriteLine(address.ToString()); // Shows Street (66) in 100,9
También se puede usar la instrucción with para crear una copia como en record. [C# 10] el operador de la izquierda puede ser un tipo anónimo.
Interfaces
Es posible crear interfaces con implementaciones por defecto, por ejemplo en caso de querer ampliar una interfaz existente se puede añadir un nuevo metódo con una implementación por defecto por lo que las clases que implementen esta interfaz no se verán afectadas.
Por ejemplo, teniendo una interfaz como:
public interface IActions
{
public double Walk();
public double Run();
}
De hecho ahora es posible tener cualquier modificador de acceso así como variables y funciones estáticas, por ejemplo para parametrizar el uso del código cliente cuando use la interfaz o incluso que los métodos puedan ser sobreescritas marcándolos como protegidos como se puede ver en la función DefaultFlight(…)
public interface IActions
{
private static int targetDistance = 0;
public static void SetTravel(int travel)
{
if (travel > 10)
targetDistance = 20;
else
targetDistance = 30;
}
public double Walk();
public double Run();
/// <summary>
/// Implementación por defecto para clases antiguas.
/// </summary>
public double Flight(int miles) => DefaultFlight(this);
protected static double DefaultFlight(IActions actions)
{
return 10 * targetDistance;
}
}
En este caso cremos una clase que implemente la primera versión de la interfaz pero no la segunda:
public class Lion : IActions
{
public double Run()
{
return 2;
}
public double Walk()
{
return 1;
}
}
En este caso para poder acceder a este código por defecto tendríamos que usar directamente la referencia a la interfaz que es la que tiene el código:
IActions ilion = lion as IActions;
Console.WriteLine(ilion.Flight(0));
Miembros virtuales en interfaces (C# 11)
Ahora se pueden definir operadores sobrecargados u otros miembros estáticos, y estas interfaces a su vez se pueden usar como restricciones para crear tipos genéricos que usan operadores o métodos estáticos. Por ejemplo podríamos hacer esto:
public interface IGetNext<T> where T : IGetNext<T>
{
static abstract T operator ++(T other);
}
Y podríamos usarlo en un struct así:
public struct RepeatSequence : IGetNext<RepeatSequence>
{
private const char Ch = 'A';
public string Text = new string(Ch, 1);
public RepeatSequence() {}
public static RepeatSequence operator ++(RepeatSequence other)
=> other with { Text = other.Text + Ch };
public override string ToString() => Text;
}
Esto se puede usar para crear algoritmo matemáticos genéricos, aquí un ejemplo completo:
public record Translation<T>(T XOffset, T YOffset) : IAdditiveIdentity<Translation<T>, Translation<T>>
where T : IAdditionOperators<T, T, T>, IAdditiveIdentity<T, T>
{
public static Translation<T> AdditiveIdentity =>
new Translation<T>(XOffset: T.AdditiveIdentity, YOffset: T.AdditiveIdentity);
}
public record Point<T>(T X, T Y) : IAdditionOperators<Point<T>, Translation<T>, Point<T>>
where T : IAdditionOperators<T, T, T>, IAdditiveIdentity<T, T>
{
public static Point<T> operator +(Point<T> left, Translation<T> right) =>
left with { X = left.X + right.XOffset, Y = left.Y + right.YOffset };
}
En este ejemplo se puede ver como se usan las interfaces IAdditionOperators<,> que devuelve el elemento identidad y IAdditiveIdentity<,> que contiene los operadores a sobrecargar. Estos cambios han traído de la mano:
Crear el operador >>> que permite que permite evitar el casteo de numeros con signo y sin signo.
Operador shift relajado, ahora no es necesario que en el operador shift el operando sea int.
Operadores checked y unchecked, ahora se pueden utilizar con operadores definidos por el usuario.
Registros (C#9)
Con C#9 tenemos un nuevo tipo de estructura, los registros, estos intentan reforzar la idea de tipos inmutables pero siendo tipos por referencia, en concreto los registros son tipos por referencia inmutables (aunque se pueden hacer mutables) pero teniendo en cuenta que al intentar comparar dos registros con los mismos valores indicará que son iguales.
En el siguiente ejemplo teniendo dos estructuras con los mismos valores, el resultado será «Equals» incluso si se crea un objecto por copia.
PersonStruct personStruct1 = new PersonStruct("John", "Doe");
PersonStruct personStruct2 = new PersonStruct("John", "Doe");
if (personStruct1.Equals(personStruct2)) // GetHashCode()->True
Console.WriteLine("Equals");
else
Console.WriteLine("Not equals");
PersonRecord personRecordCopy = new PersonRecord(personRecord1);
if (personRecordCopy == personRecord1)
// By copy
Console.WriteLine("Equals");
else
Console.WriteLine("Not equals");
Aunque es necesario inicializar todas las propiedades sin embargo no será posible modificar propiedades, por ejemplo esto dará error.
personStruct1.Name = "Other name"; // CS0200
De hecho los registros aceptan herencia (y sealed), aunque un hijo heredado tenga los mismo valores será diferente.
Incluso se puede crear un registro sin declarar explícitamente las propiedades (registros posicionales), por ejemplo:
public record Animal(string colour);
Esto crea un registro que contiene una propiedad llamada colour y que se inicializa creando un objeto que tendrá un constructor con un parámetro.
También se puede heredar de este registro aunque para eso habrá que sobreescribir algunos métodos y propiedades para indicar si ese registro es igual o no a otros tipos registros cuando se hagan comparaciones.
Aquí se puede destacar la propiedad EqualityContract, esta propiedad devuelve el tipo de la clase hija de tal forma que si el tipo de la hija es el mismo que el del padre y las propiedades son iguales los dos objetos se considerarán iguales.
public record Base(string Foo);
public record Child(string Foo, string Bar) : Base(Foo)
{
protected override Type EqualityContract => typeof(Base);
}
var b = new Base("Foo");
var c = new Child("Foo", "Bar");
Console.WriteLine(b == c); // True
Los registros soportan características adicionales como uso de Deconstruct() y expresiones with, como detalle con un with se puede crear un objeto exactamente igual. [C# 10] el operador de la izquierda puede ser un tipo anónimo. A la hora de usar with se puede personalizar la copia para que sea por valor en lugar de referencia, en ese caso solo hay que pasar al constructor un parámetro del mismo tipo que el registro y asignar los valores.
PersonRecord clone = person with { }; // Creates a copy
dynamic personRecordCopy = person with { };
Los registros incluyen varios métodos sintetizados.
Métodos para comparaciones basados en valor
Equals, ==, != y el nuevo EqualityContract
Sobreescritura de GetHashCode()
Constructor por copia y Clone()
El constructor por copia ya comentado.
Clone() realmente no tiene este nombre, es generado internamente pero tampoco se puede generar un método Clone() con ese nombre.
PrintMembers() y ToString()
PrintMembers() saca un listado de todas las propiedades del registro
ToString() similar a PrintMembers() con un poco de información adicional
[C#10] Los registros pueden ser por tipo referencia (record class) o por valor (record struct) así mismo también se puede indicar que son de solo lectura (readonly record). Aquí que tener en cuenta que por referencia o valor no tienen el mismo comportamiento.
// We define a readonly record struct with 3 properties in a single line
public readonly record struct School(string address, int number, double distance);
[C#10] También se puede hacer sealed sobre el método ToString() en clases derivadas para asegurar que todas las clases usarán la misma implementación.
public record class TeacherRecord : PersonRecord
{
public sealed override string ToString() => Subject;
}
[C#12] Ahora se puede usar ref readonly principalmente para referencias de solo lectura que se crearon antes de que existiese in
Aquí incluyo expresiones que son relativamente nuevas o no muy conocidas, no pretende ser un listado formal ni tampoco preciso en el término expresiones.
Propiedades
Se pueden usar expresiones para asociar código a varios elementos incluyendo propiedades (auto properties), en este ejemplo se asocia código a la propiedad Name usando una variable de clase (o variable miembro), este código devolverá el valor de la variable o lo establecerá con el valor proporcionado. También se puede usar para funciones y otros elementos de código.
Otro asunto diferente son los inicializadores de propiedad donde la propiedad simplemente coge un valor inicial por defecto como se ve en la propiedad Surname.
public class Sample
{
private string name = "First";
public string Name
{
get => name;
set => name = value;
}
public string Surname { get; set; } = "Surname";
}
Modificador required
Este es un nuevo modificador que obliga a los consumidores de una clase a utilizar las propiedades marcadas como required esto evita tener que crear constructores personalizados para cada combinación y también evita tener que crear un excesivo código en las clases heredadas llamando a los constructores.
public class Person
{
// The default constructor requires that FirstName and LastName be set at construction time
public required string FirstName { get; init; }
public string MiddleName { get; init; } = "";
public required string LastName { get; init; }
}
Condición ternaria por referencia
Es conocida la expresión: a = exp ? b : c; donde se asigna a la variable a el resultado de b o c dependiendo de la expresión exp, si es cierta se asignará b y si es falsa se asignará c. Por ejemplo:
var a = 12;
var b = 3;
var c = 6;
int res = (a > 10) ? b : c;
Tendría este equivalente usando la sentencia if:
int a = 12;
int res = 0;
if(a > 10)
{
res = b;
}
else
{
res = c;
}
También es posible hacerlo usando referencias en lugar de paso por valor, por ejemplo:
var a = 12;
var b = 3;
var c = 6;
ref int res = ref (a > 10) ? ref b : ref c;
res = -1; // Ahora c vale -1
En este caso se devuelve c, pero no el valor de c, es decir, se devuelve la referencia por lo tanto la última línea de código está modificando c no la variable res.
Null coalescing
Más conocida es esta expresión:
object b = a ?? new string("Null!");
En este caso si la variable a es null la variable b cogerá el valor de la expresión, en este caso new string(«Null!») esto es equivalente a:
if (a == null)
{
b = new string("Null!");
}
También muy útil para verificar parámetros nulos en métodos y lanzar excepciones:
object a = null;
object b = a ?? throw new ArgumentNullException("Object null.");
También es posible asignar directamente el valor, por ejemplo esta expresion:
if (variable is null)
{
variable = expression;
}
Es equivalente a:
variable ??= expression;
Parámetro out
Muchas veces sobre todo para hacer casting entre tipos se usa la variable out lo que obliga a declarla previamente, ya no es necesario hacer:
int i = -1;
if (int.TryParse(numericValue, out i))
return i;
return -1;
Se puede hacer lo siguiente:
if (int.TryParse(numericValue, out int i))
return i;
return -1;
Interpolación
Es conocida la posibilidad de usar de usar el operador $ para sustituir variables en cadenas de caracteres al estilo String.Format, por ejemplo:
var value = 100.435678M;
WriteLine($"You called with => {value:N3}"); // Saca 100.436
WriteLine($"\n\n{"*",-3} Hola {"*",3}"); // Imprime '* Hola *'
A partir de la versión 8, este operador se puede combinar con $ en cualquier orden, por lo que $@ y @$ son intercambiables.
[C# 10] Es posible también crear interpoladores personalizados, los valores de la interpolación llaman a String.Format() ahora este comportamiento se puede personalizar.
[C#10] También se puede hacer con constantes, solo con cadenas que también sean constantes y no con números.
[C#11] Ahora se pueden agregar líneas en la interpolación, por ejemplo:
string message = $"The usage policy for {safetyScore} is {
safetyScore switch
{
> 90 => "Unlimited usage",
> 80 => "General usage, with daily safety check",
> 70 => "Issues must be addressed within 1 week",
> 50 => "Issues must be addressed within 1 day",
_ => "Issues must be addressed before continued use",
}
}";
[C# 11] Incluir literales en cadenas, como:
int X = 2;
int Y = 3;
var pointMessage = $"""The point "{X}, {Y}" is {Math.Sqrt(X * X + Y * Y):F3} from the origin""";
Console.WriteLine(pointMessage);
Raw string literals
Hay más control sobre los literales de cadena, se crea una nueva forma usando «»» (triple comilla) que colocará el texto exactamente como se indica, la triple comilla tiene que ir sola al principio y al final, el resto del texto puede ir indentado, las comillas finales marcan la primera columna, no puede haber texto colocado antes.
string longMessage = """
This is a long message.
It has several lines.
Some are indented
more than others.
Some should start at the first column.
Some have "quoted text" in them.
""";
Indicar utf8
Ahora se puede indicar que una cadena tiene que ser utf8:
Muchas veces se escribe código para comprobar si hay valores nulos, por ejemplo:
object d = null;
var a = new { A = new { B = new { C = d } } };
if(a != null)
if(a.A != null)
if(a.A.B != null)
if(a.A.B.C != null)
Console.WriteLine($"{nameof(a.A.B.C)} = {C.ToString()}");
Esto se puede simplicar usando el operador ? el código anterior quedaría así (se incluye el opeador nameof() como ejemplo de uso, este operador coge el nombre de la variable para evitar tener que escribirlo). El código equivalente sería:
var a = new { A = new { B = new { C = d } } };
Console.WriteLine($"{nameof(C)} = {a?.A?.B?.C?.ToString()}");
Discard
A veces se crean variables temporales que no se van a usar o se pasan parámetros que no se necesitan en el contexto de una llamada, para este tipo de escenarios existe la opción de descartar esos valores y no usarlos.
bool b = int.TryParse(integer, out int _);
if (b)
{
Console.WriteLine("Can be casted.");
}
else
{
Console.WriteLine("It can not be casted.");
}
También en la devolución de una función (lo que incluye también por ejemplo expresiones lambdas) pero teniendo en cuenta que no puede haber más de un discard en el mismo ámbito (sería como crear dos variables con el mismo nombre).
public double ReturnDiscard(double value)
{
var _ = Math.Sqrt(value++);
return Math.Ceiling(_);
}
Conversion de valores
Se puede comparar el tipo y castear un valor dentro de una estructura if directamente, se usará el primero casting que funcione.
public string ConvertUsingIf(object element)
{
if (element is string s)
return s.ToUpper();// Casting included in the same line
else if (element is int i)
return Math.Abs(i).ToString();
else if (element is double d)
return Math.Truncate(d).ToString();
else
return string.Empty;
}
Console.WriteLine(structureControl.ConvertUsingIf("Element"));
Console.WriteLine(structureControl.ConvertUsingIf(5));
Console.WriteLine(structureControl.ConvertUsingIf(4.98));
/* El resultado es:
ELEMENT
5
4
*/
Con la sentencia switch se puede conseguir el mismo resultado
public string ConvertToStringUsingSwitch(object element)
{
switch (element)
{
case string s:
return s.ToUpper();
case int i:
return Math.Abs(i).ToString();
case double d:
return Math.Truncate(d).ToString();
default:
return string.Empty;
}
}
Console.WriteLine(structureControl.ConvertToStringUsingSwitch("Element"));
Console.WriteLine(structureControl.ConvertToStringUsingSwitch(5));
Console.WriteLine(structureControl.ConvertToStringUsingSwitch(4.98));
/* El resultado es:
ELEMENT
5
4
*/
De hecho con switch incluso podemos añadir algunas condiciones dentro de la conversion.
public string ConvertToStringUsingSwitchWhen(object element)
{
switch (element)
{
case string s when s.Length > 0:
return s.ToUpper();
case int i when i > 5: // Entero y mayor de 5
return Math.Abs(i).ToString();
case int i: // Cualquier entero
return Math.Sqrt(i).ToString();
case double d:
return Math.Truncate(d).ToString();
case var o when (o?.ToString().Length ?? 0) == 0:// Usando 'var'
return "Empty";
case null: // Comprobando contra 'null'
throw new ArgumentNullException(paramName: nameof(element), message: "Element must not be null");
default:
return string.Empty;
}
}
Patrones recursivos
Se añade más potencia a los patrones, ahora los patrones de expresiones son recursivos por lo que se pueden aplicar expresiones de patrones a expresiones de patrones.
Para los siguientes ejemplos se usará esta clase:
public class Building : IDisposable
{
public int Age { get; set; }
public string State { get; set; }
public void Deconstruct(out int age, out string state)
{
age = Age;
state = State.ToLower();
}
public void Dispose()
{
Age = 0;
}
}
Expresiones Switch
Ahora hay una forma más compacta de expresar un switch.
public Color ExpressionPatterns() =>
Console.BackgroundColor switch
{
ConsoleColor.Red => Color.FromArgb(255, 0, 0),
ConsoleColor.Green => Color.FromArgb(0, 255, 0),
ConsoleColor.Blue => Color.FromArgb(0, 0, 255),
_ => throw new ArgumentException("Invalid value")
};
[C# 10] Es posible ahora hacer referencia a propiedades anidadas.
Es posible usar switch a través de las propiedades de un objeto, por ejemplo con una clase Person que tenga una propiedad Age. Esto devolvería un resultado en función del valor de la propiedad Age.
public int PropertyExpression(Building p) =>
p switch
{
{ Age: 10 } => p.Age * 1,
{ Age: 20 } => p.Age * 2,
_ => 30,
};
Patrones posicionales
Se puede combinar la funcionalidad de deconstrucción con patrones posicionales dentro de sentencias switch, es decir, se crea el método Deconstruct y se usa dentro de una sentencia switch parecido al manejo de tuplas:
public string PositionalPattern(Building building)
=> building switch
{
(0, "") => "No born",
(15, "A") => "Young female",
(25, "B") => "Too much older man",
(_, _) => "No human",
_ => "Unknown"
};
Poco a poco se ha ido mejorando el uso de patrones y ahora se permite simplificaciones adicionales, por ejemplo:
public bool PatternMatchFunction(string a) =>
a[0] is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z' and not default(char));
El ejemplo anterior usamos paréntesis para remarcar la precedencia y además se permite también comprobación contra null (x is not null).
Patrones en listas
Ahora se puede hacer encajar un array o lista contra una secuencia de patrones.
int[] numbers = { 1, 2, 3 };
Console.WriteLine(numbers is [1, 2, 3]); // True
Console.WriteLine(numbers is [1, 2, 4]); // False
Console.WriteLine(numbers is [0 or 1, <= 2, >= 3]); // True
O también se puede hacer
List<int> numbers = new() { 1, 2, 3 };
if (numbers is [var first, _, _])
{
Console.WriteLine($"The first element of a three-item list is {first}.");
}
// Output:
// The first element of a three-item list is 1.
Excepciones
Es posible establecer condiciones dentro de las claúsulas catch para poder filtrar por condiciones concretas:
public void RaiseException(string code)
{
try
{
throw new ArgumentException(code);
}
catch (ArgumentException ae) when (ae.Message.Contains("100"))
{// Mismo tipo (ArgumentException) y valor ("100")
System.Console.WriteLine($"Error => {ae.Message}");
}
catch (ArgumentException ae) when (ae.Message.Contains("200"))
{
System.Console.WriteLine($"Error => {ae.Message}");
}
catch (ArgumentException ae)
{// Cualquier otro ArgumentException no incluido antes
System.Console.WriteLine($"Error => {ae.Message}");
}
}
Using
Ahora la sentencia using no está limitada por las llaves, durará lo mismo que el ámbito en el que esté contenida la variable creada por using.
El siguiente código devolverá un 0 en su propiedad Age debido al método Dispose de la clase Building.
public Building ShortUsing()
{
using Building b = new Building()
{
Age = 20,
State = "WA"
};
b.Age = 40;
return b;
}
También se pueden crear using globales, no es más que una forma de declarar un using en un fichero que se aplicará de forma global en todo el proyecto. Una forma útil es crear un fichero solo para los using.
global using System;
[C# 12] Ahora se puede usar using para cualquier tipo no solo tipos con nombre, por ejemplo tuplas, arrays, punteros… por ejemplo:
// The alias is to `List<...>` which is itself not a nullable
// reference type itself, even though it contains one as a type argument.
using Y = System.Collections.Generic.List<string?>;
using Z = int?;
Namespace [C#10]
Ahora es posible eliminar las llaves asociadas a los namespace, así se gana un poco de tabulación y espacio hacia la izquierda. Tip: Solo con poner ; al final del namespace VS limpiará los corchetes.
namespace UpToDate.Demos;
internal class DemoExpression
{...}
Secuencias asincrónicas
Es posible usar await para un bucle que consume una función asincrónica, por ejemplo teniendo este método.
public static async IAsyncEnumerable<string> GenerateWord(string a)
{
for (int i = 65; i < 97; i++)
{
await Task.Delay(100);
a += (char)i;
yield return a;
}
}
Se podría consumir directamente de esta forma:
await foreach (var word in Expressions.GenerateWord("z"))
{
Console.WriteLine(word);
}
Índices y rangos
Un índice define un elemento dentro de una secuencia, por ejemplo ^0 se refiere al primer elemento, mientras que un rango define una serie de elementos dentro de la secuencia, por ejemplo 0..8 indica los elementos del 0 al 7 ya que el primer elemento siempre se incluye y el último se omite.
var numbers = new string[]
{
"One",
"Two",
"Three",
"Four",
"Five"
};
var last = numbers[^1]; // The last one ("Five)
var rangeFromBegin = numbers[0..3]; // The first three ("One", "Two", "Three")
var rangeFromEnd = numbers[^2..^1]; // The penultimate element ("Four")
var all = numbers[..]; // All ("One","Two","Three","Four","Five")
var fromBegin = numbers[..2]; // First two ("One","Two")
var middle = numbers[3..]; // From third till end ("Four", "Five")
Range range = 1..4;
var range = numbers[range]; // From second till third ("Two", "Three", "Four")
Index index = ^2;
var index = numbers[index]; // The penultimate element ("Four")
Expresiones en colecciones
Una forma sencilla de agrupar colecciones, sirve para tipos array, Span<> y ReadOnlySpan<> y tipos que se pueden inicializar como colecciones (por ejemplo List<T>), en este ejemplo se concatenan 3 arrays.
Una forma de establecer valores a las propiedades durante la creación del objeto, es útil para inicializar en lugar de usar un constructor o establecer valores en propiedades desde clases derivadas.
Por ejemplo el siguiente código permite inicializar un objeto a través de sus propiedades y después cambiar algunas de ellas menos la marcada como init que dará un error.
public class Building
{
...
public string Name { get; init; } = "None";
...
}
Building b = new () { Age = 5, Name = "Chrysler", State = "USA" };
b.Age = 40;
b.Name = "11";
// CS8852
Scoped
Este es un modificador para tipos por valor (en concreto ref struct) que asegura que el código no extenderá el tiempo de vida de la variable. Solo se puede aplicar a variables locales y parámetros.
Span<char> values = stackalloc char[3] { 'T', 'o', 'm' };
new Test().TestMethod(values);
ref struct Test
{
public void TestMethod(scoped ReadOnlySpan<char> characters)
{
// The body of the method must only use characters in the local scope, and cannot assign it directly to any field or classes unles they themselves would be scoped.
}
}
Constructores (C# 9)
Se pueden realizar algunas simplificaciones en los constructores si el tipo está bien establecido, por ejemplo está función:
public Building IsNull(Building b)
{
if(b is null)
return new();
else
return b;
}
Se podría llamar usando:
if (expressions.IsNull(new()) is null)
Console.WriteLine("Is null");
else
Console.WriteLine("Is not null");
En todos estos casos donde se usa new() no hace falta indicar el tipo porque está bien definido ya sea a través de los parámetros, el valor de retorno o la propia construcción del objeto.
Constructores primarios
Los constructores primarios permiten declaraciones más compactas eliminando propiedades y añadiéndolas directamente en el constructor, ahora se admiten para estructuras, registros y clases. Por ejemplo lo que antes era:
public readonly struct Distance
{
public readonly double Magnitude { get; }
public readonly double Direction { get; }
public Distance(double dx, double dy)
{
Magnitude = Math.Sqrt(dx * dx + dy * dy);
Direction = Math.Atan2(dy, dx);
}
}
Ahora se puede hacer asi:
public readonly struct Distance(double dx, double dy)
{
public readonly double Magnitude { get; } = Math.Sqrt(dx * dx + dy * dy);
public readonly double Direction { get; } = Math.Atan2(dy, dx);
}
Y podemos asimismo heredar estos constructores o crear varios constructores primarios.
public class EmpireState(decimal height, decimal weight) : Building("WA")
{
public EmpireState(decimal height, decimal weight, string address) : this(height, weight) { }
}
Lambda
Se incorpora el concepto de tipo natural de tal forma que ya no hace falta indicar expresamente los tipos de parámetros o devolución, por ejemplo:
var parse = (string s) => int.Parse(s); // Equivalent to Func<string, int>
Aunque en algunos casos si se quiere indicar se puede indicar el parámetro de devolución:
var choose = object (bool b) => b ? 1 : "two"; // Func<bool, object>
Ahora también se pueden añadir atributos a la expresión lambda, a parámetros de entrada y al valor de retorno, por ejemplo:
[C# 12] Ahora también se admite parámetros por defecto, por ejemplo:
var IncrementBy = (int source, int increment = 1) => source + increment;
Atributos Genéricos (C# 11)
Ahora se pueden crear atributos genéricos, por ejemplo:
public class GenericCustomAttribute<T> : Attribute { }
Que luego podemos usar como atributo a un método por ejemplo:
[GenericCustom<string>()]
public string ShowCustomAttribute()
{
return "Custom operation string";
}
A la hora de usar al el atributo tiene que estar construido, (no puede volver a ser genérico) y hay algunas restricciones como que no se puede usar dynamic, string?, (int x, int y)… pero se pueden usar los sustitutos naturales (object, string, ValueTuple<int, int>).
Atributo Experimental
Se puede marcar código como experimental para que saque una advertencia, realmente el compilador mostrará un error que se puede evitar usando la directiva #pragma
[Experimental("DiagID", UrlFormat = "https://example.org/{0}")]
public class GenericCustomAttribute<T> : Attribute { }
#pragma warning disable DiagID
[GenericCustom<string>()]
public string ShowCustomAttribute()
{
return "Custom operation string";
}
En adelante vamos a usar el usuario pi para los ejemplos que es el que viene por defecto en la instalación, si quisiéramos usar otro podríamos crearlo con:
sudo adduser acme
Igual puede interesar crear un usuario solo para validarse por red pero no para poder iniciar sesión como un usuario normal, algo parecido a una cuenta de servicio.
Y ahora establecemos una contraseña de red para que este usuario se pueda conectar a la carpeta compartida:
sudo smbpasswd -a acme
Ahora creamos la carpeta de red en el fichero /etc/samba/smb.conf
[acme]
comment = La carpeta secreta del grupo Acme
path = /home/pi/acme
read only = No
valid users = acme
write list = acme # Usuarios que pueden escribir
create mask = 0660 # Mascara de creación de archivos
directory mask = 0770 # Máscara de creación de directorios
Con este comando verificamos que todo está correcto:
testparm
Hay que dar los permisos adecuados al usuario que hemos creado sobre la carpeta compartida, por ejemplo:
sudo chown -R pi:pi /home/pi/acme
Si todo es correcto reiniciamos Samba:
sudo service smbd restart
Si tenemos algún problema al configurar o conectar al recurso remoto podemos usar el comando smbclient para poder ver los recursos compartidos de un servidor, este comando nos pedirá una contraseña para poder acceder y nos sacará los recursos compartidos.
smbclient -L coyote.acme.hell
Cliente
Ahora hay que configurar y probar el lado cliente, ya sea desde Linux o desde Windows.
Raspberry Pi
Desde el lado cliente en una Raspberry se pueden establecer valores de permisos a ficheros y directorios a través de las opciones file_mode y dir_mode en el fichero /etc/fstab usando los valores a establecer en octal, estos serán los permisos que se establecerán al montar la unidad, un ejemplo de entrada fstab sería:
La opción vers sirve para indicar la versión del protocolo SMB a usar, si da errores al montar por parámetros incorrectos una opción puede ser establecer una versión más baja, 1.0, 2.0 o 3.0
Windows
Ahora conectamos al recurso remoto de la forma que elijamos y puede ser que obtengamos el siguiente error en Windows.
Es una advertencia de seguridad indicando que no se permite acceder a recursos remotos que permitan invitados, este comportamiento empezó en la versión 2.0 del protocolo SMB.
Es importante intentar acceder al servidor con el nombre completo, es decir, si tenemos un servidor que se llama server en un dominio home.local deberíamos usar \\server.home.local
Hay dos formas de evitar este error:
Una que es la más práctica es usando la opción de mapear unidades de red de Windows indicando explícitamente usuario y contraseña.
Es necesario también indicar la ruta completa el servidor a la hora de mapear la unidad, es decir \\servidor\carpeta
Si no se quiere usar el dominio por defecto se puede indicar como usuario .\pi al poner .\ se usará como dominio la máquina local.
La otra opción es deshabilitando este error, a través de Directiva de equipo local (gpedit.msc), en esta ruta:
Configuración de equipo local\plantillas administrativas\Red\Estación de trabajo Lanman\Habilitar inicios de sesión de invitado no seguros
Habilitamos esta opción lo que evitará el mensaje.