Declaração de sintaxe de retorno ímpar


106

Sei que pode parecer estranho, mas não sei nem como pesquisar essa sintaxe na internet e também não tenho certeza do que significa exatamente.

Eu observei algum código MoreLINQ e então notei 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;
        }
    }
}

O que é esta estranha declaração de retorno? return _();?


6
Ou você quer dizer return _(); IEnumerable<TSource> _():?
Alex K.

6
@Steve, será que o OP está se referindo mais ao return _(); IEnumerable<TSource> _()que ao yield return?
Rob

5
Eu acho que ele quis dizer esta linha return _(); IEnumerable<TSource> _(). Ele pode ficar confuso com a aparência, e não com a instrução de retorno real.
Mateusz

5
@AkashKava O OP disse que havia uma declaração de retorno estranha. Infelizmente, o código contém duas instruções de retorno. Portanto, é compreensível que as pessoas fiquem confusas sobre a que ele se refere.
mjwills

5
Editou a pergunta, e mais uma vez desculpe a confusão.
kuskmen

Respostas:


106

Este é o C # 7.0 que oferece suporte a funções locais ....

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 # atual com 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(); 

    }

O truque é: _ () é declarado após ser usado, o que é perfeitamente normal.

Uso prático de funções locais

O exemplo acima é apenas uma demonstração de como o método embutido pode ser usado, mas provavelmente se você for invocar o método apenas uma vez, ele não terá utilidade.

Mas no exemplo acima, conforme mencionado nos comentários de Phoshi e Luaan , há uma vantagem em usar a função local. Visto que a função com retorno de rendimento não será executada a menos que alguém a itere, neste caso o método fora da função local será executado e a validação do parâmetro será realizada mesmo se ninguém iterar o valor.

Muitas vezes repetimos o código no método, vamos ver este exemplo ..

  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... 
  }

Eu poderia otimizar isso com ...

  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... 
  }

4
@ZoharPeled Bem .. o código postado faz mostrar um uso para a função .. :)
Rob

2
@ColinM um dos benefícios é que a função anônima pode acessar facilmente as variáveis ​​de seu 'host'.
mjwills

6
Tem certeza de que em C #, isso é realmente chamado de função anônima? Parece ter um nome, a saber _AnonymousFunctionou apenas _, enquanto eu esperaria que uma função anônima genuína fosse algo assim (x,y) => x+y. Eu chamaria isso de função local, mas não estou acostumado com a terminologia C #.
chi

12
Para ser explícito, como ninguém parece ter apontado, este trecho de código está usando a função local porque é um iterador (observe o rendimento) e, portanto, executa lentamente. Sem a função local, você precisaria aceitar que a validação de entrada acontece no primeiro uso ou ter um método que só será chamado por um outro método por muito pouco motivo.
Phoshi

6
@ColinM O exemplo kuksmen postado é na verdade um dos principais motivos pelos quais isso foi finalmente implementado - quando você faz uma função com yield return, nenhum código é executado até que o enumerável seja realmente enumerado. Isso é indesejável, pois você deseja, por exemplo, verificar os argumentos imediatamente. A única maneira de fazer isso em C # é separando o método em dois métodos - um com yield returns e outro sem. Os métodos embutidos permitem que você declare o yieldmétodo using internamente , evitando confusão e uso indevido potencial de um método que é estritamente interno ao seu pai e não reutilizável.
Luaan

24

Considere o exemplo mais simples

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

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

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

_() é uma função local declarada dentro do método que contém a instrução de retorno.


3
Sim eu conheço as funções locais foi a formatação que me enganou ... espero que não fique normal.
kuskmen

20
Você quer dizer que a declaração da função começa na mesma linha? Se sim, concordo, é horrível!
Stuart

3
Sim, foi isso que eu quis dizer.
kuskmen

9
Exceto pelo nome, o sublinhado também é horrível
Icepickle

1
@AkashKava: a questão não é se é C # legal, mas se o código é fácil de entender (e, portanto, fácil de manter e agradável de ler) quando formatado dessa forma. As preferências pessoais desempenham um papel, mas tendo a concordar com Stuart.
PJTraill de
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.