Declaración de sintaxis de retorno impar

106

Sé que esto puede sonar extraño, pero ni siquiera sé cómo buscar esta sintaxis en Internet y tampoco estoy seguro de qué significa exactamente.

Así que miré un poco de código MoreLINQ y luego noté este método

public static IEnumerable<TSource> DistinctBy<TSource, TKey>(this IEnumerable<TSource> source,
        Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer)
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (keySelector == null) throw new ArgumentNullException(nameof(keySelector));

    return _(); IEnumerable<TSource> _()
    {
        var knownKeys = new HashSet<TKey>(comparer);
        foreach (var element in source)
        {
            if (knownKeys.Add(keySelector(element)))
                yield return element;
        }
    }
}

¿Qué es esta extraña declaración de devolución? return _();?

kuskmen
fuente
6
¿O te refieres a return _(); IEnumerable<TSource> _():?
Alex K.
6
@Steve, me pregunto si el OP se refiere más al return _(); IEnumerable<TSource> _()que al yield return?
Rob
5
Creo que se refería a esta línea return _(); IEnumerable<TSource> _(). Podría confundirse por la forma en que se ve en lugar de la declaración de devolución real.
Mateusz
5
@AkashKava El OP dijo que había una declaración de devolución extraña. Desafortunadamente, el código contiene dos declaraciones de retorno. Por lo tanto, es comprensible que las personas estén confundidas sobre a qué se refiere.
mjwills
5
Edité la pregunta y, una vez más, lamento la confusión.
kuskmen

Respuestas:

106

Este es C # 7.0 que admite funciones locales ...

public static IEnumerable<TSource> DistinctBy<TSource, TKey>(
       this IEnumerable<TSource> source,
        Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer)
    {
        if (source == null) throw new 
           ArgumentNullException(nameof(source));
        if (keySelector == null) throw 
             new ArgumentNullException(nameof(keySelector));

        // This is basically executing _LocalFunction()
        return _LocalFunction(); 

        // This is a new inline method, 
        // return within this is only within scope of
        // this method
        IEnumerable<TSource> _LocalFunction()
        {
            var knownKeys = new HashSet<TKey>(comparer);
            foreach (var element in source)
            {
                if (knownKeys.Add(keySelector(element)))
                    yield return element;
            }
        }
    }

C # actual con Func<T>

public static IEnumerable<TSource> DistinctBy<TSource, TKey>(
       this IEnumerable<TSource> source,
        Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer)
    {
        if (source == null) throw new 
           ArgumentNullException(nameof(source));
        if (keySelector == null) throw 
             new ArgumentNullException(nameof(keySelector));

        Func<IEnumerable<TSource>> func = () => {
            var knownKeys = new HashSet<TKey>(comparer);
            foreach (var element in source)
            {
                if (knownKeys.Add(keySelector(element)))
                    yield return element;
            }
       };

        // This is basically executing func
        return func(); 

    }

El truco es que _ () se declara después de que se usa, lo cual está perfectamente bien.

Uso práctico de funciones locales

El ejemplo anterior es solo una demostración de cómo se puede usar el método en línea, pero lo más probable es que si va a invocar el método solo una vez, entonces no sirve de nada.

Pero en el ejemplo anterior, como se mencionó en los comentarios de Phoshi y Luaan , existe la ventaja de usar la función local. Dado que la función con retorno de rendimiento no se ejecutará a menos que alguien la repita, en este caso se ejecutará el método fuera de la función local y se realizará la validación del parámetro incluso si nadie iterará el valor.

Muchas veces hemos repetido código en el método, veamos este ejemplo.

  public void ValidateCustomer(Customer customer){

      if( string.IsNullOrEmpty( customer.FirstName )){
           string error = "Firstname cannot be empty";
           customer.ValidationErrors.Add(error);
           ErrorLogger.Log(error);
           throw new ValidationError(error);
      }

      if( string.IsNullOrEmpty( customer.LastName )){
           string error = "Lastname cannot be empty";
           customer.ValidationErrors.Add(error);
           ErrorLogger.Log(error);
           throw new ValidationError(error);
      }

      ... on  and on... 
  }

Podría optimizar esto con ...

  public void ValidateCustomer(Customer customer){

      void _validate(string value, string error){
           if(!string.IsNullOrWhitespace(value)){

              // i can easily reference customer here
              customer.ValidationErrors.Add(error);

              ErrorLogger.Log(error);
              throw new ValidationError(error);                   
           }
      }

      _validate(customer.FirstName, "Firstname cannot be empty");
      _validate(customer.LastName, "Lastname cannot be empty");
      ... on  and on... 
  }
Akash Kava
fuente
4
@ZoharPeled Bueno .. el código publicado hace mostrar un uso para la función .. :)
Rob
2
@ColinM uno de los beneficios es que la función anónima puede acceder fácilmente a las variables desde su 'host'.
mjwills
6
¿Estás seguro de que en C #, esto se llama función anónima? Parece tener un nombre, a saber _AnonymousFunctiono simplemente _, mientras que esperaría que una función anónima genuina sea algo así (x,y) => x+y. Yo llamaría a esto una función local, pero no estoy acostumbrado a la terminología de C #.
chi
12
Para ser explícito, como nadie parece haberlo señalado, este fragmento de código está usando la función local porque es un iterador (tenga en cuenta el rendimiento) y, por lo tanto, se ejecuta de forma perezosa. Sin la función local, necesitaría aceptar que la validación de entrada ocurre en el primer uso, o tener un método que solo será llamado por otro método por muy pocas razones.
Phoshi
6
@ColinM El ejemplo que kuksmen publicó es en realidad una de las principales razones por las que finalmente se implementó: cuando crea una función con yield return, no se ejecuta ningún código hasta que el enumerable se enumera realmente. Esto no es deseable, ya que desea, por ejemplo, verificar los argumentos de inmediato. La única forma de hacer esto en C # es separando el método en dos métodos: uno con yield returns y el otro sin. Los métodos en línea le permiten declarar el yieldmétodo using en su interior , evitando el desorden y el posible uso indebido de un método que es estrictamente interno a su padre y no reutilizable.
Luaan
24

Considere el ejemplo más simple

void Main()
{
    Console.WriteLine(Foo()); // Prints 5
}

public static int Foo()
{
    return _();

    // declare the body of _()
    int _()
    {
        return 5;
    }
}

_() es una función local declarada dentro del método que contiene la declaración de retorno.

Stuart
fuente
3
Sí, sé acerca de las funciones locales, fue el formato lo que me engañó ... espero que esto no se convierta en estándar.
kuskmen
20
¿Te refieres a la declaración de función que comienza en la misma línea? Si es así, estoy de acuerdo, ¡es horrible!
Stuart
3
Sí, eso es lo que quise decir.
kuskmen
9
Excepto por eso, nombrarlo subrayado también es horrible
Icepickle
1
@AkashKava: la pregunta no es si es C # legal, sino si el código es fácil de entender (y por lo tanto fácil de mantener y agradable de leer) cuando se formatea así. Las preferencias personales juegan un papel, pero tiendo a estar de acuerdo con Stuart.
PJTraill