martes, 11 de agosto de 2015

Novedades C# 6.0 (parte 1)




Vamos a hacer una pequeña pausa dentro de nuestro curso de LinQ, y aprovechando que la nueva versión de Visual Studio, del Framework y de C#, ya están en la calle, daremos un repaso a las nuevas características que nos trae el lenguaje del ‘C Sostenido’.

De primeras podemos decir que los cambios introducidos dentro de la nueva versión del lenguaje nativo de la plataforma .Net, no son, ni demasiado bestias, ni demasiado amplios, pero contribuyen y favorecen a una mejor lectura, compresión y organización de nuestro código.


Para intentar hacer que los posts, sean lo más didácticos posibles, pondré cada uno de los ejemplos de manera inicial, como se hacían antes (versión 5.0 de C#) y como lo podremos hacer ahora (versión 6.0 de C#) para poder ilustrarnos con las diferencias.




Entregas anteriores de novedades C# 6.0

Parte 1






Antes de comenzar con la tarea, me gustaría recalcar, que los ejemplos están pensados para la comprensión, y para no alargar ni ensuciar el código, nos hemos saltado reglas de arquitectura, validación, etc.

El procedimiento que voy a seguir dentro de este post, es mostrar una primera clase, realizada completamente en C# 5.0 e ir repasando una primera tanda de novedades para esta nueva entrega, que a la vez, nos hagan irla transformando, para al final del todo contemplar su resultado en C# 6.0.



La clase

La clase que he generado para la ocasión, se llamada FilesMto, esta clase, tendría la misión de realizar un mantenimiento rápido dentro un grupo de ficheros. Vamos a mostrarla y la comentamos de manera rápida.

public class FilesMto_5
{
    private List<string> _ficheros;
    public List<string> Ficheros
    {
        get { return _ficheros; }
    }

    public Dictionary<string, string> Config { get; set; } // Guarda datos sobre el formato fecha y directorio de backup

    public FilesMto_5(params string[] archivos)
    {
        _ficheros = archivos.ToList();

        CargarConfiguracionPorDefecto();
    }

    public void CargarConfiguracionPorDefecto()
    {
        Config = new Dictionary<string, string>();

        Config.Add("FormatoFecha", "ddMMyyyymmss");
        Config.Add("RutaBackup", @"C:\"); // no añado una ruta diferente a C:\ para no tener que comprobar y crear y ensuciar el código
    }

    public bool ExistenFicheros(string[] files)
    {
        return ! files.Any(f => ! File.Exists(f));
    }

    public void HacerBackup(string[] files)
    {
        files.ToList().ForEach(f => File.Copy(f, CrearDirectorioBackup(f)));
    }

    public string CrearDirectorioBackup(string archivo)
    {
        string fichero = Path.GetFileNameWithoutExtension(archivo);
        string extension = Path.GetExtension(archivo);
        string resultado = Path.Combine(Config["RutaBackup"], string.Format("{0}_{1}{2}", fichero, DateTime.Now.ToString(Config["FormatoFecha"]), extension));

        return resultado;
    }
}



Lo primero que nos llama la atención al ver la clase, es un ‘_5’. Le he añadido este sufijo, para diferenciarla, de la que generaremos para la nueva versión, que por supuesto tendrá el sufijo ‘_6’.

La clase, prácticamente es autoexplicativa, pero en si está formada por:


2 propiedades:
  • Ficheros: Guarda la lista con las rutas de los ficheros a operar.
  • Config: Guarda la configuración con la que tratar dichos ficheros.


Un constructor y 3 métodos con nombre que definen bastante bien su función en el código.

Volver a insistir que muchas partes del código podrían haberse mejorado, pero que han sido elegidas así, por su mayor nivel instructivo.



Using Static

En esta nueva versión, se ha añadido la posibilidad de realizar en la parte destinada a la importación de namespaces, la facultad de hacer using de clases añadiendo la palabra reservada static.  Con ello podremos concebir una importación de los métodos estáticos de la clase.

Voy a intentar explicarme un poquito mejor, poniendo un ejemplo algo más clarificador, para cada una de ellas:


En este ejemplo, utilizamos las 2 clases estáticas más famosas del framework, que no son otras que System.Console y System.Math:

Esta sería una clase de prueba que realizaría una impresión en pantalla del valor absoluto de un número entero:


using System;

namespace NovedadesCSharp6_1
{
    public static class UsingStatic5
    {

        public static void ImprimirAbsoluto()
        {
            int valor = -12;

            int absValor = Math.Abs(valor);

            Console.WriteLine(absValor);
        }
    }
}


 Para ver las diferencias y la reducción de código que se realiza en esta nueva versión, nada mejor que ver su actualización para 6.0:


using System;
using static System.Console;
using static System.Math;

namespace NovedadesCSharp6_1
{
    public class UsingStatic6
    {
        public static void ImprimirAbsoluto()
        {
            int valor = -12;

            int absValor = Abs(valor);

            WriteLine(absValor);
        }
    }
}


Como podemos distinguir, hemos eliminado la cualificación de las clases, dentro de las llamadas a los métodos estáticos:


Math   .Abs(valor);         --> Abs(valor);
Console.WriteLine(absValor) --> WriteLine(absValor);


Como trasladamos todo esto a nuestra clase, pues de la siguiente manera:


Simplemente con añadir los using static, para las clases System.IO.File y System.IO.Path, visual studio 2015, ya nos dará pistas, anunciándonos de que hay código que nos sobra, y que podemos eliminar.




Así luciría la clase, con nuestro primer retoque:


using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using static System.IO.File;
using static System.IO.Path;

namespace FileUtils
{
    public class FilesMto_5
    {
        private List<string> _ficheros;
        public List<string> Ficheros
        {
            get { return _ficheros; }
        }
        public Dictionary<string, string> Config { get; set; } // Guarda datos sobre el formato fecha y directorio de backup

        public FilesMto_5(params string[] archivos)
        {
            _ficheros = archivos.ToList();

            CargarConfiguracionPorDefecto();
        }

        public void CargarConfiguracionPorDefecto()
        {
            Config = new Dictionary<string, string>();

            Config.Add("FormatoFecha", "ddMMyyyymmss");
            Config.Add("RutaBackup", @"C:\"); // no añado una ruta diferente a C:\ para no tener que comprobar y crear y ensuciar el código
        }

        public bool ExistenFicheros(string[] files)
        {
            /// return ! files.Any(f => ! File.Exists(f)); -------- File. ELIMINADO
            return ! files.Any(f => ! Exists(f));  
        }

        public void HacerBackup(string[] files)
        {
            /// files.ToList().ForEach(f => File.Copy(f, CrearDirectorioBackup(f))); -------- File. ELIMINADO
            files.ToList().ForEach(f => Copy(f, CrearDirectorioBackup(f)));
        }

        public string CrearDirectorioBackup(string archivo)
        {
            /// string fichero   = Path.GetFileNameWithoutExtension(archivo);  --------- Path.  ELIMINADO
            string fichero   = GetFileNameWithoutExtension(archivo);
            /// string extension = Path.GetExtension(archivo);  --------- Path.  ELIMINADO
            string extension = GetExtension(archivo);
            /// string resultado = Path.Combine(Config["RutaBackup"], string.Format("{0}_{1}{2}", fichero, DateTime.Now.ToString(Config["FormatoFecha"]), extension)); 
            ///      ------------   Path.  ELIMINADO
            string resultado = Combine(Config["RutaBackup"], string.Format("{0}_{1}{2}", fichero, DateTime.Now.ToString(Config["FormatoFecha"]), extension));

            return resultado;
        }
    }
}


He añadido unos comentarios con las sentencias originales, para que se pueda apreciar mejor el cambio.


Auto Property Initializer y Dictionary Initializer

Estas son dos propiedades  que debería haber explicado de forma separada, pero dado que tienen cosas en común y que están vinculadas la una con la otra, dentro del update de nuestra clase, las vamos a explicar de una vez.


Las Auto Property Initializer, tienen dos peculiaridades. La primera de ellas es la posibilidad de permitir realizar Propiedades de solo lectura. Esto antes no era posible, y solo estaba permitido tener campos de solo lectura, mediante la palabra reservada readonly, pero está no estaba disponible con las propiedades. Para emularlas teníamos que valernos de algo como así:


namespace NovedadesCSharp6_1
{
    public class MiClase_5
    {
        private int _miPropiedad;
        public int MiPropiedad
        {
            get { return _miPropiedad; }
        }

        public MiClase_5(int _propiedadInt)
        {
            _miPropiedad = _propiedadInt;
        }

    }
}


Creábamos un campo, al que le vinculábamos una propiedad que solo contenía la llamada al set.

A alguien se le puede pasar por la cabeza que antes también podíamos utilizar las propiedades automáticas, pero para poder emplearlas, éstas tenían que tener las dos llamadas al get y al set, en caso de faltar alguna de ellas, nuestro visual studio no nos dejaría compilar el código.


public int MiPropiedad { get; set; }


En C# 6.0, podemos tener una propiedad con solo una llamada al get contraído, de manera que nuestra propiedad sea de solo lectura y solo pueda ser modificada desde el constructor de nuestra clase:

namespace NovedadesCSharp6_1
{
    public class MiClase_6
    {
        public int MiPropiedad { get; }

        public MiClase_6(int _propiedadInt)
        {
            MiPropiedad = _propiedadInt;
        }
    }
}

La segunda peculiaridad, radica en la posibilidad de inicializar nuestras propiedades automáticas de forma autónoma, sin tener que hacer uso del constructor de nuestra clase.

En el pasado, esta era la manera:


namespace NovedadesCSharp6_1
{
    public class MiClase_5
    {
        public int MiPropiedad { get; set; }

        public MiClase_5()
        {
            MiPropiedad = 25;
        }
    }
}

Como podemos contemplar, hacemos uso del constructor para inicializar la propiedad MiPropiedad con el valor 25.

Esa limitación también se ha visto superada en esta nueva versión y ahora el código se reduciría a esto:


namespace NovedadesCSharp6_1
{
    public class MiClase_6
    {
        public int MiPropiedad { get; set; } = 25;
    }
}

Turno para el Dictionary Initializer, que no es otra cosa que un inicializador automático de Dictionaries. En las versiones anteriores, teníamos formas de inicializar clases, arrays, listas, etc de forma automática:

var miClase = new MiClase_5 { MiPropiedad = 25 };

var array = new int[] { 1, 2, 3, 4 };

var lista = new List<int>() { 1, 2, 3, 4 };

Ahora podemos hacer algo muy parecido a lo que hacemos con las otras colecciones, así:


ictionary<string, object> claveValor = new Dictionary<string, object>() {["PropiedadInt"] = 2,["PropiedadDateTime"] = DateTime.Today };

Muy sencillito.


Vamos a continuar tuneando nuestra clase, para ello nos vamos a centrar en esta porción de código:


public Dictionary<string, string> Config { get; set; } // Guarda datos sobre el formato fecha y directorio de backup

public FilesMto_5(params string[] archivos)
{
    _ficheros = archivos.ToList();

    CargarConfiguracionPorDefecto();
}
public void CargarConfiguracionPorDefecto()
{
    Config = new Dictionary<string, string>();

    Config.Add("FormatoFecha", "ddMMyyyymmss");
    Config.Add("RutaBackup", @"C:\"); // no añado una ruta diferente a C:\ para no tener que comprobar y crear y ensuciar el código
}


Este código está comprendido por la propiedad que guarda la configuración y su alimentación mediante la llamada al método CargarConfiguracionPorDefecto dentro del constructor.


Aquí nos podemos quitar la llamada al método CargarConfiguraciónPorDefecto del constructor, inicializando automáticamente la propiedad Config así:


public Dictionary<string, string> Config { get; set; } = new Dictionary<string, string>() {["FormatoFecha"] = "ddMMyyyymmss",["RutaBackup"] = @"C:\" };


También podemos reducir el campo y la propiedad Ficheros de solo lectura:


private List<string> _ficheros;
public List<string> Ficheros
{
    get { return _ficheros; }
}


Dejándolo así, como hemos visto antes:


public List<string> Ficheros { get; }


Ya se va apreciando la reducción en nuestra clase:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using static System.IO.File;
using static System.IO.Path;

namespace FileUtils
{
    public class FilesMto_5
    {
        public List<string> Ficheros { get; }
        public Dictionary<string, string> Config { get; set; } = new Dictionary<string, string>() {["FormatoFecha"] = "ddMMyyyymmss",["RutaBackup"] = @"C:\" };

        public FilesMto_5(params string[] archivos)
        {
            Ficheros = archivos.ToList();
        }

        public bool ExistenFicheros(string[] files)
        {
            return ! files.Any(f => ! Exists(f));  
        }

        public void HacerBackup(string[] files)
        {
            files.ToList().ForEach(f => Copy(f, CrearDirectorioBackup(f)));
        }

        public string CrearDirectorioBackup(string archivo)
        {
            string fichero   = GetFileNameWithoutExtension(archivo);
            string extension = GetExtension(archivo);
            string resultado = Combine(Config["RutaBackup"], string.Format("{0}_{1}{2}", fichero, DateTime.Now.ToString(Config["FormatoFecha"]), extension));

            return resultado;
        }
    }
}


Nuevo Formateo de Strings

Esta novedad, no va a tener un impacto de reducción de código demasiado significativo dentro de nuestra clase, pero puede ser una de las novedades más prácticas de todas las que forman C# 6.0. Bajo mi experiencia, el método estático String.Format, ofrece una serie de ventajas a la hora de formatear strings muy considerables, y no me estoy refiriendo solo a versatilidad y opciones, hablo también de rendimiento, ese rendimiento que a veces nos juega malas pasadas al operar con strings y con cualquier clase inmutable. El 98% de las veces, este método se emplea para concatenar datos, y dada la falta de claridad en sus sintaxis, si no lo conoces un poquito más a fondo, la mayoría de la gente suele inclinarse por la utilización del operador más (+).


Vamos a ver un ejemplo de lo que os comento:


string concatenacion1 = "El valor mínimo de un string es " + int.MinValue + " y el máximo es " + int.MaxValue;

string concatenacion2 = string.Format("El valor mínimo de un string es {0} y el máximo es {1}", int.MinValue, int.MaxValue);


La primera línea normalmente es mucho más sencilla de seguir, que la segunda.

Pues hecha esta pequeña introducción, vamos a ver la mejora, que no es otra que la ‘fusión’ de ambas. Simplemente con colocar el carácter de dólar ($), precediendo a las dobles comillas del string, podemos introducir variables directamente entre llaves, sin tener que utilizar el operador +, y sin tener que emplear la llamada al string.Format, del mismo modo que realizamos para las cadenas que contienen directorios con una @ y así no tener que salvar todas las ‘\’:


string concatenacion3 = $"El valor mínimo de un string es {int.MinValue} y el máximo es {int.MaxValue}";


Con esta mejora, solo podremos reducir un poquito una de las líneas del código de nuestra clase. Es esta:


string resultado = Combine(Config["RutaBackup"], string.Format("{0}_{1}{2}", fichero, DateTime.Now.ToString(Config["FormatoFecha"]), extension));


Así se quedaría:


string resultado = Combine(Config["RutaBackup"], $"{fichero}_{DateTime.Now.ToString(Config["FormatoFecha"])}{extension}");




Expression Bodied Methods

Esta nueva mejora, ataca directamente a la reducción de líneas de código, y permite el poder definir métodos de una clase con la sintaxis de una Expressión Lambda. Mi recomendación para su empleo, es no utilizarlo nunca para funciones que tengan más de una línea de código, ya que al implicar la escritura de las llaves, su función se pierde por completo.

En las versiones anteriores hacíamos esto:


public static void DiHola()
{
    Console.WriteLine("Holaaa !!!");
}

Y ahora lo dejaríamos en una sola línea, así:


public static void DiHolaEnUnaLinea() => Console.WriteLine("Holaaa !!!");


Puntualizar una cosa, cuando nuestro método tiene parámetros, es obligatorio poner el tipo dentro de los paréntesis de la Lambda, ya que al compilador le es imposible inferir el tipo:


public static void DiAlgoEnUnaLinea(string algo) => Console.WriteLine(algo);


Y aquí la simplificación de código en nuestra clase:


using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using static System.IO.Path;
using static System.IO.File;

namespace FileUtils
{
    public class FilesMto_5
    {
        public List<string> Ficheros { get; }
        public Dictionary<string, string> Config { get; set; } = new Dictionary<string, string>() {["FormatoFecha"] = "ddMMyyyymmss",["RutaBackup"] = @"C:\" };
        // no añado una ruta diferente a C:\ para no tener que comprobar y crear y ensuciar el código

        public FilesMto_6(params string[] archivos)
        {
            Ficheros = archivos.ToList();
        }

        //public void CargarConfiguracionPorDefecto()
        //{
        //    Config = new Dictionary<string, string>() {["FormatoFecha"] = "dd/MM/yyyy",["RutaBackup"] = @"C:\Backups" };
        //}

        public void CargarConfiguracionPorDefecto() => new Dictionary<string, string>() {["FormatoFecha"] = "dd/MM/yyyy",["RutaBackup"] = @"C:\Backups" };

        //public bool ExistenFicheros(string[] files)
        //{
        //    return ! files.Any(f => ! File.Exists(f));
        //}

        public bool ExistenFicheros(IEnumerable<string> files) => ! files.Any(f => !Exists(f));


        //public void HacerBackup(string[] files)
        //{
        //    files.ToList().ForEach(f => File.Copy(f, CrearDirectorioBackup(f)));
        //}

        public void HacerBackup(IEnumerable<string> files) => files.ToList().ForEach(f => Copy(f, CrearDirectorioBackup(f)));

        public string CrearDirectorioBackup(string archivo)
        {
            string fichero   = GetFileNameWithoutExtension(archivo);
            string extension = GetExtension(archivo);
            string resultado = Combine(Config["RutaBackup"], $"{fichero}_{DateTime.Now.ToString(Config["FormatoFecha"])}{extension}");

            return resultado;
        }
    }
}


He dejado comentado los métodos modificados para una mejor comprensión



Resultado

Pues nada aquí el resultado, de la primera (antigua) vs la última (nueva), para que podamos ver mejor la diferencia.


Antigua:


using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace FileUtils
{
    public class FilesMto_5
    {
        private List<string> _ficheros;
        public List<string> Ficheros
        {
            get { return _ficheros; }
        }
        public Dictionary<string, string> Config { get; set; } // Guarda datos sobre el formato fecha y directorio de backup

        public FilesMto_5(params string[] archivos)
        {
            _ficheros = archivos.ToList();

            CargarConfiguracionPorDefecto();
        }

        public void CargarConfiguracionPorDefecto()
        {
            Config = new Dictionary<string, string>();

            Config.Add("FormatoFecha", "ddMMyyyymmss");
            Config.Add("RutaBackup", @"C:\"); // no añado una ruta diferente a C:\ para no tener que comprobar y crear y ensuciar el código
        }

        public bool ExistenFicheros(string[] files)
        {
            return !files.Any(f => !File.Exists(f));
        }

        public void HacerBackup(string[] files)
        {
            files.ToList().ForEach(f => File.Copy(f, CrearDirectorioBackup(f)));
        }

        public string CrearDirectorioBackup(string archivo)
        {
            string fichero = Path.GetFileNameWithoutExtension(archivo);
            string extension = Path.GetExtension(archivo);
            string resultado = Path.Combine(Config["RutaBackup"], string.Format("{0}_{1}{2}", fichero, DateTime.Now.ToString(Config["FormatoFecha"]), extension));

            return resultado;
        }
    }
}


Nueva:


using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using static System.IO.Path;
using static System.IO.File;

namespace FileUtils
{
    public class FilesMto_6
    {
        public List<string> Ficheros { get; }
        public Dictionary<string, string> Config { get; set; } = new Dictionary<string, string>() {["FormatoFecha"] = "ddMMyyyymmss",["RutaBackup"] = @"C:\" };
        // no añado una ruta diferente a C:\ para no tener que comprobar y crear y ensuciar el código

        public FilesMto_6(params string[] archivos)
        {
            Ficheros = archivos.ToList();
        }

        public void CargarConfiguracionPorDefecto() => new Dictionary<string, string>() {["FormatoFecha"] = "dd/MM/yyyy",["RutaBackup"] = @"C:\Backups" };

        public bool ExistenFicheros(IEnumerable<string> files) => ! files.Any(f => !Exists(f));

        public void HacerBackup(IEnumerable<string> files) => files.ToList().ForEach(f => Copy(f, CrearDirectorioBackup(f)));

        public string CrearDirectorioBackup(string archivo)
        {
            string fichero   = GetFileNameWithoutExtension(archivo);
            string extension = GetExtension(archivo);
            string resultado = Combine(Config["RutaBackup"], $"{fichero}_{DateTime.Now.ToString(Config["FormatoFecha"])}{extension}");

            return resultado;
        }
    }
}


Hasta aquí, la primera entrega de las novedades, en la siguiente me centraré en las nuevas versatilidades del operador ‘?’, que dan para mucho, muchísimo.