Nieparzysta instrukcja składni powrotu

106

Wiem, że może to brzmieć dziwnie, ale nie wiem nawet, jak przeszukać tę składnię w Internecie, a także nie jestem pewien, co dokładnie oznacza.

Więc obejrzałem trochę kodu MoreLINQ, a potem zauważyłem tę metodę

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

Co to za dziwne polecenie zwrotu? return _();?

kuskmen
źródło
6
Czy masz na myśli return _(); IEnumerable<TSource> _():?
Alex K.
6
@Steve, zastanawiam się, czy OP odnosi się bardziej do return _(); IEnumerable<TSource> _()niż yield return?
Rob
5
Myślę, że miał na myśli tę kwestię return _(); IEnumerable<TSource> _(). Mógłby być zdezorientowany wyglądem, a nie faktyczną deklaracją zwrotu.
Mateusz
5
@AkashKava OP powiedział, że wystąpiła dziwna instrukcja zwrotu. Niestety kod zawiera dwie instrukcje powrotu. Jest więc zrozumiałe, jeśli ludzie nie wiedzą, do czego się odnoszą.
mjwills
5
Zredagowałem pytanie i jeszcze raz przepraszam za zamieszanie.
kuskmen

Odpowiedzi:

106

To jest C # 7.0, który obsługuje funkcje lokalne ...

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

Bieżący C # z 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(); 

    }

Sztuczka polega na tym, że _ () jest deklarowane po jej użyciu, co jest całkowicie w porządku.

Praktyczne wykorzystanie funkcji lokalnych

Powyższy przykład to tylko demonstracja, w jaki sposób można użyć metody inline, ale najprawdopodobniej jeśli zamierzasz wywołać metodę tylko raz, to nie będzie to przydatne.

Ale w powyższym przykładzie, jak wspomniano w komentarzach Phoshi i Luaan , istnieje zaleta korzystania z funkcji lokalnej. Ponieważ funkcja z yield return nie zostanie wykonana, chyba że ktoś ją iteruje, w tym przypadku zostanie wykonana metoda poza funkcją lokalną, a walidacja parametrów zostanie wykonana, nawet jeśli nikt nie będzie iterował wartości.

Wiele razy powtarzaliśmy kod w metodzie, spójrzmy na ten przykład.

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

Mógłbym to zoptymalizować za pomocą ...

  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
źródło
4
@ZoharPeled Cóż .. oddelegowany kod robi pokazują zastosowanie dla funkcji .. :)
Rob
2
@ColinM jedną z zalet jest to, że funkcja anonimowa może łatwo uzyskać dostęp do zmiennych ze swojego „hosta”.
mjwills
6
Czy jesteś pewien, że w języku C # jest to faktycznie nazywane funkcją anonimową? Wydaje się, że ma nazwę, a mianowicie _AnonymousFunctionlub po prostu _, podczas gdy spodziewałbym się, że prawdziwa anonimowa funkcja będzie czymś podobnym (x,y) => x+y. Nazwałbym to funkcją lokalną, ale nie jestem przyzwyczajony do terminologii C #.
chi
12
Mówiąc wprost, jak się wydaje, nikt tego nie zauważył, ten fragment kodu używa funkcji lokalnej, ponieważ jest iteratorem (zwróć uwagę na wydajność), więc wykonuje się leniwie. Bez funkcji lokalnej musiałbyś albo zaakceptować, że sprawdzanie poprawności danych wejściowych ma miejsce przy pierwszym użyciu, albo mieć metodę, która będzie wywoływana tylko przez jedną inną metodę z bardzo niewielkiego powodu.
Phoshi
6
@ColinM Przykład opublikowanego kuksmen jest właściwie jednym z głównych powodów, dla których zostało to ostatecznie zaimplementowane - kiedy tworzysz funkcję za pomocą yield return, żaden kod nie jest wykonywany, dopóki nie zostanie wyliczony. Jest to niepożądane, ponieważ chcesz np. Od razu zweryfikować argumenty. Jedynym sposobem, aby to zrobić w C #, jest rozdzielenie metody na dwie metody - jedną z yield returns, a drugą bez. Metody wbudowane pozwalają zadeklarować yieldmetodę using wewnątrz , unikając bałaganu i potencjalnego niewłaściwego użycia metody, która jest ściśle wewnętrzna dla jej rodzica i nie nadaje się do ponownego użycia.
Luaan
24

Rozważmy prostszy przykład

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

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

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

_() jest funkcją lokalną zadeklarowaną w metodzie zawierającej instrukcję return.

Stuart
źródło
3
Tak, wiem o lokalnych funkcjach, to formatowanie mnie oszukało ... mam nadzieję, że to nie stanie się standardem.
kuskmen
20
Czy masz na myśli deklarację funkcji zaczynającą się w tej samej linii? Jeśli tak, zgadzam się, to straszne!
Stuart
3
Tak, o to mi chodziło.
kuskmen
9
Poza tym nazwanie go podkreśleniem jest również okropne
Icepickle
1
@AkashKava: nie chodzi o to, czy jest to legalny C #, ale czy kod jest łatwy do zrozumienia (a zatem łatwy w utrzymaniu i przyjemny do czytania) po sformatowaniu w ten sposób. Osobiste preferencje odgrywają rolę, ale zwykle zgadzam się ze Stuartem.
PJTraill