Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 25 de abril de 2017
C#Pues ya con la séptima versión de C# en la calle, aparecida de la mano del flamante Visual Studio 2017, va siendo hora de echar un vistacillo rápido a las principales novedades que encontraremos en esta nueva iteración de nuestro lenguaje favorito.

Y hoy vamos a comenzar con las funciones locales, una nueva capacidad que nos permitirá crear funciones locales a un ámbito, y que no serán visibles desde fuera de éste. Por ejemplo, podemos crear funciones en el interior de métodos, constructores, getters o setters, etc., pero éstas sólo serán visibles desde el interior del miembro en el que han sido declaradas.

Puede ser útil en determinados escenarios, puesto que evitan la introducción de "ruido" en las clases cuando determinado código sólo se va a consumir en el interior de un método, y al mismo tiempo pueden contribuir a mejorar la legibilidad y robustez del código.

El punto de partida

Veamos un ejemplo. Imaginad el siguiente método donde se reciben unos parámetros de entrada que deben ser "corregidos" según una lógica determinada antes de ser sumados:
public class Foo
{
   ... // Other members of Foo

   public int CalculateSomething(int n, int m)
   {
       var fixedN = n < 0 ? 0 : n < 11 ? 10 : n < 21 ? 20 : n % 20;
       var fixedM = m < 0 ? 0 : m < 11 ? 10 : m < 21 ? 20 : m % 20;
       return fixedN + fixedM;
   }
}
Como podéis ver, el resultado no es nada bonito. Tenemos una lógica duplicada que complicará el mantenimiento y puede provocar errores en el futuro.

Obviamente, podríamos mejorar bastante este código si extraemos dicha lógica a un método dentro de la propia clase Foo:
public int CalculateSomething(int n, int m)
{
    var fixedN = FixValueForCalculate(n);
    var fixedM = FixValueForCalculate(m);
    return fixedN + fixedM;
}

private int FixValueForCalculate(int n)
{ 
    return n < 0 ? 0 : n < 11 ? 10 : n < 21 ? 20 : n % 20;
}
Sin embargo, si FixValueForCalculate() sólo se va a utilizar en el interior de CalculateSomething(), estaríamos añadiendo métodos a la clase cuando realmente no le aportan nada, pues esa lógica no tiene aplicación más allá del método desde que la consume. Correríamos el riesgo de que se usara desde otros métodos (esto podría provocar daños colaterales si la modificamos), y añadiría ruido en las ayudas de codificación como intellisense.

Otra posibilidad sería crear helpers, o métodos estáticos en una clase de utilidad, pero estaríamos creando más artefactos y separando la lógica del único punto desde el que se usa… tampoco es esto una solución del todo razonable.

Y por falta de posibilidades que no sea… en C# también podríamos hacer un apaño utilizando delegados,  como en el siguiente bloque de código:
public int CalculateSomething(int n, int m)
{
    Func<int, int> fixValue = k => k < 0 ? 0 : k < 11 ? 10 : k < 21 ? 20 : k % 20;
    var fixedN = fixValue(n);
    var fixedM = fixValue(m);
    return fixedN+fixedM;
}
Aunque soluciona, este enfoque también tiene sus desventajas. Aparte de sutiles allocations (que podrían ser importantes en entornos donde el rendimiento sea fundamental), también sería problemático, o al menos farragoso de escribir, si la lógica fuera recursiva porque las lambdas no lo ponen fácil.

Funciones locales en C#

Bien, pues C# ataja este escenario mediante las funciones locales. Se declaran en el interior del miembro desde las que las usamos, y sólo son visibles desde él:
public int CalculateSomething(int n, int m)
{
    var fixedN = FixValue(n);
    var fixedM = FixValue(m);
    return fixedN+fixedM;

    int FixValue(int k)
    {
        return k < 0 ? 0 : k < 11 ? 10 : k < 21 ? 20 : k % 20;
    }
}
Observad que la sintaxis para declararlas es muy intuitiva, idéntica a los métodos tradicionales excepto en que no incluyen modificadores de visibilidad (private, public…), y su ubicación en el interior de un bloque de código.

Pero como estamos en la era de los expression bodied members y en este caso la función retorna simplemente la evaluación de una expresión, también podemos escribirlo usando este dulce azucarillo sintáctico. Fijaos que parece una lambda (Func<T1, T2>), pero no lo es:
public int CalculateSomething(int n, int m)
{
    var fixedN = FixValue(n);
    var fixedM = FixValue(m);
    return fixedN+fixedM;

    int FixValue(int k) => k < 0 ? 0 : k < 11 ? 10 : k < 21 ? 20 : k % 20;
}

Algunas características interesantes de las funciones locales

Las funciones locales pueden ser definidas en cualquier punto del código de métodos, constructores, getters o setters, ya sea antes o después de ser utilizadas. Es decir, aunque no es muy recomendable por los problemas de legibilidad que introduce, el siguiente código compilaría bien:
public int Foo()
{
    var x = Bar(); 

    int Bar()      
    {
        ...
    }

    var y = Bar(); 
    return x + y;
}
Dado que se encuentran en un ámbito concreto, pueden capturar variables de éste y utilizarlas en su interior, como en el siguiente ejemplo:
public int Foo()
{
    var x = 2;
    return Multiply(3); // Returns 6

    int Multiply(int y) => x * y;
}
Como era de esperar, también pueden ser asíncronas:
public async Task<int> Foo(int x)
{
    return x * await GetValue();

    async Task<int> GetValue()
    {
        await Task.Delay(1000);
        return 2;
    }
}
Y aunque probablemente tenga poca utilidad práctica, como curiosidad, es interesante saber que pueden ser declaradas en bloques de código específicos, restringiendo aún más su ámbito de uso:
public int Foo(int a, int b, int c)
{
    if (a > b)
    {
        return Multiply(a, b);
        int Multiply(int x, int y) => x * y;
    }
    return Multiply(b, c); // Compilation error!!
}

¿Y puedo usar esta característica en Visual Studio 2015?

Estrictamente hablando sí se puede, aunque la experiencia no será del todo satisfactoria. Veamos cómo hacerlo partiendo de un código de una aplicación de consola como el siguiente, creado desde Visual Studio 2015, que inicialmente no compilará:
class Program
{
    static void Main(string[] args)
    {
        var four = Sum(2, 2);
        Console.WriteLine(four);
        Console.ReadLine();

        int Sum(int x, int y) => x + y;
    }
}
Visual Studio indicando que existen errores sintácticosSin embargo, si instalamos el paquete Nuget "Microsoft.Net.Compilers" (es posible que requiera reiniciar Visual Studio), veremos que el IDE muestra el típico subrayado rojo indicando que existen errores, sin embargo el proyecto compilará y funcionará correctamente.

En proyectos MVC 5, por ejemplo, dicho paquete Nuget ya está instalado, por lo que lo único que tendremos que hacer es actualizarlo a la última versión.

Publicado en Variable not found.

6 Comentarios:

Apertil dijo...

Que buen artículo!!!!..como siempre ;-)
Gracias José Maria.

José María Aguilar dijo...

Muchas gracias a ti por comentar! :)

Gabriel Arellano dijo...

Excelente Post maestro! esperamos leer más de C#7.0 explicado tan bien.

Saludos,

José María Aguilar dijo...

Muchas gracias, Gabriel!

Sí, tengo más en el horno, iremos viendo cosillas las próximas semanas :)

Saludos!

Rfog dijo...

¡Ñapable!

Ya me gustaría a mi tener eso en C++. Código que solo se usa una vez, por mor de simplicidad, llena clases de métodos que podrían estar más localizados (sí, ya sé, haz una sub-clase con esos métodos, pero no es lo mismo).

Aunque como todo, peligroso. Ya imagino a los zarpas habituales teniendo cientos de métodos dentro de métodos haciendo lo mismo en diferentes partes de un mismo fuente cuando podrían usar un, ejem, método de extensión, ejem (ni yo mismo me creo que esté escribiendo esto. Ya solo me falta decir que la aberración de la dualidad onda-partícula, digo que un value class herede de un reference class así por las buenas, sea algo lógico... Debe ser la edad).

José María Aguilar dijo...

Hola!

Pues sí, como todo en este mundo, cada herramienta tiene escenarios donde aplican y donde no.

Las mejoras en el lenguaje se crean pensando en los primeros, pero obviamente pueden ser mal utilizadas; pero vaya, al final, el que quiere liarla en el código se busca las fórmulas para conseguirlo :DD

Un saludo!