Compilaciones deterministas

Una buena característica que se puede añadir a una librería además de SourceLink o crear la documentación es configurar la compilación como determinista.

Esto quiere decir que compilar ensamblados bajo las mismas condiciones de entrada producirá el mismo binario equivalente byte a byte. Por condiciones de entrada se entienden:

  • La secuencia de parámetros de entrada al compilador (flags).
  • Los contenidos del archivo de respuesta del compilador .rsp.
  • La versión exacta del compilador usado así como de los ensamblados referenciados.
  • Ruta completa del directorio actual (se pueden abreviar usando relativas)
  • Contenidos binarios de todos los ficheros pasados al compilador
    • Código fuente
    • Ensamblados referenciados
    • Módulos referenciados
    • Recursos
    • El nombre del fichero de claves (strong name)
    • Archivos de respuesta @
    • Analizadores
    • Conjuntos de reglas
    • «archivos adicionales» que puedan ser usados por analizadores
  • La cultura actual (se refiere a la cultura en la que se producen los mensajes de excepción y diagnóstico)
  • La codificación por defecto (o la actual) si la codificación no se ha especificado
  • La existencia, o no existencia, y contenidos de archivos en las rutas de búsqueda del compilador ( p.ej /lib, /recurse)
  • La plataforma CLR en la cual el compilador funciona (esto se nota por ejemplo al calcular precisiones en algunos tipos numéricos)
  • El valor de %LIBPATH% ya que esto puede afectar la carga del analizador de dependencias
  • Ahora mismo el compilador también depende de la hora del día y números aleatorios generados para GUID que no es determinístico si no se especifica /deterministic.

Permitir que el binario resultante sea el mismo proporciona varias ventajas por ejemplo para mecanismos de caché y optimización de test. También asegura que tanto la herramienta de compilación como la forma en que se ha compilado es consistente y verificable tanto por si misma con respecto al código fuente utilizado.

De igual forma en compilaciones incrementales incrementa la rapidez de compilación al compilar solo algunas partes, por ejemplo los ficheros de salida que ya están actualizados respecto a los ficheros de entrada no son ejecutados.

En el mundo DevOps proporciona ciertas ventajas para saber que pasos de compilación que dependen de cambios en un binario tienen que ser ejecutados.

La opción determinista está habilitada por defecto desde VisualStudio 2017, si se quiere habilitar para una versión anterior o deshabilitarla, tendríamos que editar el fichero .csproj.

<PropertyGroup>
  <Deterministic>true</Deterministic>
</PropertyGroup>

Es posible que se quiera deshabilitar ya que la opción determinista no permite establecer un número de versión autoincremental (*) por razones obvias este valor cambia en cada compilación por lo que no es determinista.

Este valor también se puede pasar al compilador a través del flag /deterministic

DevOps

Los PDBs contienen las rutas de los ficheros que se usarán para depurar, esto en entornos locales no es un problema, pero en entornos DevOps puede ser un problema al no tener acceso a esas rutas o incluso a la máquina, para esto existe la directiva <DeterministicSourcePaths/> que debe ser establecida en true, esto hace que la compilación sea completamente determinista tanto en local como en entornos DevOps.

Configuración

Con todo lo anterior lo que habría que hacer es establecer las directivas <Deterministic/> y <ContinuousIntegrationBuild/> con los valores comentados.

Además hay que incluir la directiva <EmbedUntrackedSources /> para que se incluya también el código generado por el compilador como AssemblyInfo.cs, hay que tener en cuenta que si ya está configurado para SourceLink entonces esta directiva ya estará incluida.

Además para entornos DevOps hay que añadir una configuración adicional.

Azure DevOps

<PropertyGroup Condition="'$(TF_BUILD)' == 'true'">
  <Deterministic>True</Deterministic>
  <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>

GitHub

<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
  <Deterministic>True</Deterministic>
  <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>

Si se está usando un target de SDK inferior a 3.1.300 hará falta agregar un fichero Directory.Build.targets con la siguiente configuración, también hará falta para librerías .NetStandard (por lo menos hasta la versión 2.1 incluida).

<Project>
  <PropertyGroup>    
    <TargetFrameworkMonikerAssemblyAttributesPath>
$([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)')) 
    </TargetFrameworkMonikerAssemblyAttributesPath>
  </PropertyGroup>
  <ItemGroup>
    <EmbeddedFiles Include="$(GeneratedAssemblyInfoFile)"/>
  </ItemGroup>
</Project>

Si se está usando Coverlet entonces habrá que usar la siguiente configuración para no tener problemas durante la integración continua.

<Project>
  <PropertyGroup>    <TargetFrameworkMonikerAssemblyAttributesPath>$([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)'))</TargetFrameworkMonikerAssemblyAttributesPath>
  </PropertyGroup>
  <ItemGroup>
    <EmbeddedFiles Include="$(GeneratedAssemblyInfoFile)"/>
  </ItemGroup>
  <ItemGroup>
    <SourceRoot Include="$(NuGetPackageRoot)" />
  </ItemGroup>
  <Target Name="CoverletGetPathMap"
          DependsOnTargets="InitializeSourceRootMappedPaths"
          Returns="@(_LocalTopLevelSourceRoot)"
          Condition="'$(DeterministicSourcePaths)' == 'true'">
    <ItemGroup>
      <_LocalTopLevelSourceRoot Include="@(SourceRoot)" Condition="'%(SourceRoot.NestedRoot)' == ''"/>
    </ItemGroup>
  </Target> 
</Project>

Mapear rutas

Si no se compila en formato portable es posible que las rutas dentro de los ficheros PDB no sean correctas o cambien, es posible mapear la ruta física respecto a la ruta que aparece en los PDB mediante la directiva PathMap, como:

<PropertyGroup>
  <Deterministic>true</Deterministic>
  <PathMap>$(EnlistmentRoot)=C:\</Features>
</PropertyGroup>

Siendo EnlistmentRoot la variable que apunta al raíz del código fuente, el parámetro del compilador sería:

-pathmap:path1=sourcePath1,path2=sourcePath2

Verificar

Para probarlo localmente se puede compilar pasando la propiedad TF_BUILD al compilador (si es orientado a Azure DevOps), por ejemplo:

dotnet build /p:TF_BUILD=true

Para comprobar que todo está correcto, después de compilar empaquetamos, usando por ejemplo la opción pack del proyecto y al abrirlo con NuGet Package Explorer deberíamos ver el tick verde en Deterministic.

Esto tiene una ventana añadida, podemos verificar que el paquete es correcto antes de aplicarlo a un pipe DevOps.

Referencias

Ventajas de compilaciones deterministas
Compilaciones deterministas en Roslyn
Deterministic source paths
Compilaciones incrementales
Entradas deterministas
Problemas con versionado automático
Configuración Deterministic Build
Configuración PathMap
Opción del compilador (-pathmap)
Opción del compilador (-deterministic)
Portable PDB

Compilaciones deterministas

Reflexiones Fluent Api (I): Introducción

Reconozco que utilizar una librería con Fluent Api puede ser cómodo o según como se haga bastante incómodo, como quería dar una nueva cara a una librería que tengo pensé en hacerla con estilo Fluent Api , podía ser interesante de cara a la librería y también a su facilidad de uso.

De la creación de esa librería y de algunas otras llegué a estas conclusiones que indico aquí (sobre todo para que no se me olviden la próxima vez).

Definición

La definición de Fluent Api está clara en la Wikipedia, esto no quita que haya que tener en cuenta algunos elementos o características propias del lenguaje que se vayan a usar, también indicar que se hace una distinción entre Fluent Api y method chaining, en este caso voy a usar las dos juntas indistintamente pero es importante tener esa distinción en cuenta.

También hay que tener en cuenta que el objeto con el que se está trabajando en Fluent Api es interno a la librería y puede estar compuesto a su vez de otros objetos, la idea de Fluent Api es construir ese objeto interno de una forma correcta para su uso.

Nomeclatura

Es importante tener claro como se van a prefijar los elementos y ceñirse a esta nomenclatura, muchas librerías usan los suyos propios indico aquí un estilo de ejemplo que es el que usaré:

  • AddXXX: Cuando queremos añadir un nuevo elemento al objeto interno, por ejemplo el objeto interno tiene una lista y queremos añadir un nuevo elemento a esa lista:
  • WithXXX: Para añadir una propiedad o establecer un valor al objeto con el que estamos trabajando, puede ser el interno o un objeto dependiente de este.
  • IsXXX / HasXXX: Los dos están orientado a valores booleanos, es verdad que podríamos usar también WithXXX pero muchas veces a los valores booleanos se les da un tratamiento especial para remarcarlos. Si buscásemos una diferencia entre IsXXX y HasXXX podríamos hacer lo siguiente:
    • IsXXX: Para establecer un valor booleano en una propiedad, por ejemplo IsDeveloper, IsCEO.
    • HasXXX: Cuando necesitamos hacer una verificación para obtener esa información o la sacamos de una colección, por ejemplo HasChilds, HasDriverLicense.
  • UseXXX: UseXXX se usa generalmente cuando queremos definir un comportamiento, por ejemplo internamente tenemos varias formas de crear el objeto o de guardarlo y queremos usar una en concreto, también es muy útil para establecer configuraciones.
  • AndXXX: Cuando vamos a concatenar dos propiedades que se configuran por separado pero deben de ir juntas ya que dependen entre sí.

Estilo

En este sentido también hay algunas sugerencias, por ejemplo si la línea de código es bastante larga puede ser interesante separarla en varias líneas, por ejemplo:

var configuration = Builder.Create()
                       .AddElement()
                       .WithProperty()
                       .AndAnotherOne().Save() 

El último método que finaliza se puede colocar también en otra línea aparte o en la misma, lo que interesa remarcar principalmente es cada opción de creación en una línea separada, en este punto puede haber algún problema con los editores de código de cara al formateo y la tabulación pero con un poco de práctica a la hora de generar las líneas o cambiando las opciones de formateo de código es suficiente.

Con los parámetros sucede algo parecido, es posible que los métodos tengan muchos parámetros en ese caso un nivel de indentación adicional ayuda bastante, como se ha comentado dependiendo del editor de código no siempre el formateo de código resulta cómodo así que puede haber variaciones:

var configuration = Builder.Create()
                       .AddElement(
                            text: "Hello")
                       .WithProperty(
                            description: "big",
                            color: "green")
                       .AndAnotherOne().Save() 

Por norma general se intenta usar métodos en todos los casos aunque es verdad que a veces pueda resultar más legible usar una propiedad, realmente es raro tener métodos que no usen parámetros así que por homogeneidad es mejor usar siempre métodos.

No debería pero estas configuraciones pueden llegar a ser largas por lo que incluirlas dentro de un método de configuración en una clase de inicio de la aplicación suele ser la opción más cómoda que es básicamente el comportamiento de los métodos de creación de .Net Core.

Estructura de Fluent Api

Primero recordar lo que dice la propia definición de la Wikipedia sobre la creación de las interfaces:

  • El método de una interfaz siempre devuelve como parámetro la propia interfaz.
  • Siempre hay un método sin contexto final que termina el proceso de creación (Save(), Build(),…)
  • Se trabaja siempre sobre el mismo contexto (u objeto interno) que se construirá.

Voy a usar este ejemplo demostrativo, no pretende ser un ejemplo real simplemente está hecho para poder explicar varios conceptos en los siguientes capítulos.

Diagrama general

Hay dos interfaces, una se encarga de la gestión de usuarios y la otra de la gestión de la conexión, ambas están conectadas a través de una interfaz que representa la configuración de una base de datos.

Reflexiones Fluent Api (I): Introducción

Reflexiones Fluent Api (II): Métodos

Introducción

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.
IUsersBuilder builder = DatabaseBuilder
    .Create()
    .AddLogin()
    .WithCredential(
        username: "login",
        password: "password")
    .AndRole(
        database: "acme")
    .AddRight(
        permission: "read")
    .AddRight(
        permission: "write");
  • 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);
}

Reflexiones Fluent Api (II): Métodos

Reflexiones Fluent Api (III): Interfaces

Introducción

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.
namespace FluentApi
{
    public interface IUsersBuilder
    {
        ICredentialBuilder AddLogin();

        IGroupBuilder AddGroups();
    }

    public interface ICredentialBuilder : IUsersBuilder
    {
        IRoleBuilder WithCredential(
            string username = "",
            string password = ""
            );
    }
}
  • 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.
IDatabaseBuilder databaseBuilder = DatabaseBuilder.Create();
databaseBuilder
    .AddLogin()
    .WithCredential(
        username: "login",
        password: "password")
    .AndRole(
        database: "acme")
    .AddRight(
        permission: "read")
    .AddRight(
        permission: "write");
databaseBuilder
    .WithServer(
        name: "server",
        port: 8000,
        version: new Version("0.0.0.1"));
var json = databaseBuilder.Save();
Reflexiones Fluent Api (III): Interfaces

Reflexiones Fluent Api (IV): Clases

Introducción

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>();
}

Reflexiones Fluent Api (IV): Clases

Documentación: Configuración

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.

Documentación: Configuración

Documentación: Referencia

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 &gt;

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:

ClaveDescripción
N Namespace
T Tipo: clase, interfaz, estructura, enumerado, delegado
F Campo
P Propiedad
M Método (incluyendo constructores, operadores,…)
E Evento
¡ Cadena de error, generado por el compilador cuando hay un error

Por ejemplo, para un tipo se podría usar:

<see cref="nCubed.Core.Exceptions.SemanticException"/>

En caso de que se haga una referencia a código de .NET, para que Sandcastle lo encuentre correctamente se haría así:

<see cref="T:System.Func`1"/> to be processed.

También se puede usar para hacer referencia a un método de una interfaz genérica:

<see cref="nCubed.Core.Expressions.IAnyEnumerableCollection`1.Any"/>

Si hay dos parámetros genéricos se haría:

<see cref="nCubed.CQRS.Infrastructure.ProcessManagerRepository`2"/>

Cuando se hace referencia a métodos

<see cref="M:System.Object.Equals(System.Object)"/>

Referencia

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:

<include file='filename' path='tagpath[@name="id"]' />

Y si creamos un documento XML como:

<?xml version="1.0"?>
<CustomDoc>
    <CustomMember name="Animal">
	<summary>
		Full documentation for Animal class.
	</summary>
    </CustomMember>
</CustomDoc>

Tendríamos este resultado:

/// <include file='Docs/AnimalInclude.xml'
/// path='CustomDoc/CustomMember[@name="Animal"]/*'/>

<list>

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.

/// <summary>
/// Overrides original, 
/// <seealso cref="Person.GetHashCode()"/>.
/// </summary>

<value>

Describe el valor que representa una propiedad.

/// <value>
/// The Address property must not contain extended chars.
/// </value>

Referencias

Recommended Tags for Documentation Comments:
https://msdn.microsoft.com/es-es/library/5ast78ax.aspx
How to: Use the XML Documentation Features (C# Programming Guide)
https://msdn.microsoft.com/es-es/library/z04awywx.aspx

Documentación: Referencia

UpToDate C#: Extra

C#12

(Actualizado a 10 de Agosto de 2024)

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.

<PropertyGroup>
    <LangVersion>8.0</LangVersion>
</PropertyGroup>

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:

<PropertyGroup>
    <Nullable>enable</Nullable>
</PropertyGroup>

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:

<PropertyGroup>
    <Nullable>enable</Nullable>
</PropertyGroup>

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.

string? str = null;
if(extra.IsNullOrEmpty(str))
{
    Console.WriteLine(str.Length);
}

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.

Referencias:

Top level statements (C# 9)

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.

UpToDate C#: Extra

UpToDate C#: Funciones

C#12

(Actualizado a 10 de Agosto de 2024)

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

public void Validate(bool condition, [CallerArgumentExpression("condition")] string? message = null)
{
    if (!condition)
    {
        throw new InvalidOperationException($"Argument failed validation: <{message}>");
    }
}

bool validateSomethingInteresting = false;
try
{
    functions.Validate(validateSomethingInteresting);
}
catch(Exception e)
{
    Console.WriteLine(e.Message); // Argument failed validation: <validateSomethingInteresting>
}

Propiedades

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");
}

UpToDate C#: Funciones

UpToDate C#: Estructuras

C#12

(Actualizado a 10 de Agosto de 2024)

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:

  1. Crear el operador >>> que permite que permite evitar el casteo de numeros con signo y sin signo.
  2. Operador shift relajado, ahora no es necesario que en el operador shift el operando sea int.
  3. 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

UpToDate C#: Estructuras