Funkcja lokalna vs Lambda C # 7.0

178

Patrzę na nowe implementacje w C # 7.0 i wydaje mi się interesujące, że zaimplementowały funkcje lokalne, ale nie mogę sobie wyobrazić scenariusza, w którym funkcja lokalna byłaby preferowana nad wyrażeniem lambda i jaka jest różnica między nimi.

Rozumiem, że lambdy są anonymousfunkcjami, podczas gdy funkcje lokalne nie są, ale nie mogę rozgryźć rzeczywistego scenariusza, w którym funkcja lokalna ma przewagę nad wyrażeniami lambda

Każdy przykład byłby bardzo mile widziany. Dzięki.

Sid
źródło
9
Generiki, parametry wyjściowe, funkcje rekurencyjne bez konieczności inicjowania lambdy do wartości null itp.
Kirk Woll
5
@KirkWoll - powinieneś opublikować to jako odpowiedź.
Enigmativity

Odpowiedzi:

276

Wyjaśnił to Mads Torgersen w C # Design Meeting Notes, gdzie po raz pierwszy omówiono funkcje lokalne :

Chcesz funkcji pomocniczej. Używasz go tylko z poziomu pojedynczej funkcji i prawdopodobnie używa zmiennych i parametrów typu, które znajdują się w zakresie funkcji zawierającej. Z drugiej strony, w przeciwieństwie do lambdy, nie potrzebujesz jej jako obiektu pierwszej klasy, więc nie obchodzi Cię nadawanie jej typu delegata i przydzielanie rzeczywistego obiektu delegata. Możesz również chcieć, aby był rekurencyjny lub ogólny, lub aby zaimplementował go jako iterator.

Aby jeszcze bardziej go rozwinąć, zalety to:

  1. Występ.

    Podczas tworzenia lambdy należy utworzyć delegata, co w tym przypadku jest niepotrzebną alokacją. Funkcje lokalne to tak naprawdę tylko funkcje, nie są potrzebni żadne delegaty.

    Ponadto funkcje lokalne są bardziej wydajne przy przechwytywaniu zmiennych lokalnych: lambdy zwykle przechwytują zmienne do klasy, podczas gdy funkcje lokalne mogą używać struktury (przekazywanej za pomocą ref), co ponownie unika alokacji.

    Oznacza to również, że wywoływanie funkcji lokalnych jest tańsze i można je wbudować, co może jeszcze bardziej zwiększyć wydajność.

  2. Funkcje lokalne mogą być rekurencyjne.

    Lambdy mogą być również rekurencyjne, ale wymaga niezręcznego kodu, w którym najpierw należy przypisać nulldo zmiennej delegata, a następnie lambda. Funkcje lokalne mogą naturalnie być rekurencyjne (w tym rekurencyjne wzajemnie).

  3. Funkcje lokalne mogą być ogólne.

    Lambdy nie mogą być generyczne, ponieważ muszą być przypisane do zmiennej o konkretnym typie (ten typ może używać zmiennych ogólnych z zewnętrznego zakresu, ale to nie to samo).

  4. Funkcje lokalne mogą być implementowane jako iterator.

    Lambdy nie mogą używać słowa kluczowego yield return(i yield break) do implementacji IEnumerable<T>funkcji -returning. Funkcje lokalne mogą.

  5. Funkcje lokalne wyglądają lepiej.

    Nie jest to wspomniane w powyższym cytacie i może to być tylko moje osobiste uprzedzenie, ale myślę, że normalna składnia funkcji wygląda lepiej niż przypisanie lambda do zmiennej delegowanej. Funkcje lokalne są również bardziej zwięzłe.

    Porównać:

    int add(int x, int y) => x + y;
    Func<int, int, int> add = (x, y) => x + y;
    
svick
źródło
22
Chciałbym dodać, że funkcje lokalne mają nazwy parametrów po stronie wywołującego. Lambdy nie.
Lensflare
3
@Lensflare Prawdą jest, że nazwy parametrów lambd nie są zachowywane, ale to dlatego, że muszą zostać przekonwertowane na delegatów, którzy mają własne nazwy. Na przykład: Func<int, int, int> f = (x, y) => x + y; f(arg1:1, arg2:1);.
Svick
1
Świetna lista! Mogę sobie jednak wyobrazić, jak kompilator IL / JIT mógłby wykonać wszystkie optymalizacje wymienione w 1. również dla delegatów, jeśli ich użycie jest zgodne z określonymi regułami.
Marcin Kaczmarek
1
@Casebash Ponieważ wyrażenia lambda zawsze używają delegata, a ten delegat przechowuje zamknięcie jako plik object. Zatem lambdy mogłyby używać struktury, ale musiałaby być opakowana, więc nadal miałbyś tę dodatkową alokację.
svick
1
@happybits Przeważnie wtedy, gdy nie musisz nadawać mu nazwy, na przykład gdy przekazujesz to do metody.
Svick
83

Oprócz świetnej odpowiedzi Svicka, funkcje lokalne mają jeszcze jedną zaletę:
można je zdefiniować w dowolnym miejscu funkcji, nawet po returninstrukcji.

public double DoMath(double a, double b)
{
    var resultA = f(a);
    var resultB = f(b);
    return resultA + resultB;

    double f(double x) => 5 * x + 3;
}
Tim Pohlmann
źródło
5
Jest to bardzo przydatne, ponieważ mogę przyzwyczaić się do umieszczania wszystkich funkcji pomocniczych w #region Helpersdolnej części funkcji, aby uniknąć bałaganu w tej funkcji, a zwłaszcza uniknąć bałaganu w głównej klasie.
AustinWBryan
Ja też to doceniam. Dzięki temu główna funkcja, na którą patrzysz, jest łatwiejsza do odczytania, ponieważ nie musisz rozglądać się, aby znaleźć początek. Jeśli chcesz zobaczyć szczegóły implementacji, patrz dalej niż koniec.
Remi Despres-Smyth
3
jeśli twoje funkcje są tak duże, że potrzebują w sobie regionów, są za duże.
ssmith
9

Jeśli również zastanawiasz się, jak przetestować funkcję lokalną, powinieneś sprawdzić JustMock, ponieważ ma on odpowiednią funkcjonalność. Oto prosty przykład klasy, który zostanie przetestowany:

public class Foo // the class under test
{ 
    public int GetResult() 
    { 
        return 100 + GetLocal(); 
        int GetLocal () 
        { 
            return 42; 
        } 
    } 
}

A oto jak wygląda test:

[TestClass] 
public class MockLocalFunctions 
{ 
    [TestMethod] 
    public void BasicUsage() 
    { 
        //Arrange 
        var foo = Mock.Create<Foo>(Behavior.CallOriginal); 
        Mock.Local.Function.Arrange<int>(foo, "GetResult", "GetLocal").DoNothing(); 

        //Act 
        var result = foo. GetResult(); 

        //Assert 
        Assert.AreEqual(100, result); 
    } 
} 

Oto link do dokumentacji JustMock .

Zrzeczenie się. Jestem jednym z deweloperów odpowiedzialnych za JustMock .

Mihail Vladov
źródło
wspaniale jest widzieć takich pełnych pasji programistów, którzy opowiadają się za korzystaniem z ich narzędzi. Jak zainteresowałeś się pisaniem narzędzi programistycznych na pełny etat? Jako Amerykanin mam wrażenie, że znalezienie takiej kariery może być trudne, jeśli nie ma się magistra lub doktora. w comp sci.
John Zabroski
Cześć John i dziękuję za miłe słowa. Jako programista nie widzę nic lepszego niż bycie docenianym przez moich klientów za wartość, jaką im zapewniam. Połącz to z chęcią podjęcia ambitnej i konkurencyjnej pracy, a otrzymasz dość ograniczoną listę rzeczy, które mnie pasjonują. Na tej liście znajduje się pisanie narzędzi dla programistów zwiększających produktywność. Przynajmniej w mojej głowie :) Jeśli chodzi o karierę, to myślę, że firmy dostarczające narzędzia dla programistów to dość mały odsetek wszystkich firm programistycznych i dlatego trudniej o taką możliwość.
Mihail Vladov
Jedno oddzielne pytanie. Dlaczego nie zadzwonisz tutaj do VerifyAll? Czy istnieje sposób, aby powiedzieć JustMockowi, aby sprawdził również wywołanie funkcji lokalnej?
John Zabroski
2
Cześć @JohnZabroski, testowany scenariusz nie wymagał potwierdzania zdarzeń. Oczywiście można było sprawdzić, czy połączenie zostało wykonane. Najpierw musisz określić, ile razy spodziewasz się wywoływania metody. W ten sposób: .DoNothing().OccursOnce();A później potwierdź, że wywołanie zostało wykonane przez wywołanie Mock.Assert(foo);metody. Jeśli jesteś zainteresowany, w jaki sposób obsługiwane są inne scenariusze, możesz przeczytać nasz artykuł pomocy dotyczący potwierdzania występowania .
Mihail Vladov
0

Używam funkcji inline, aby uniknąć presji na zbieranie śmieci, szczególnie w przypadku dłuższych metod. Powiedzmy, że chciałbyś otrzymać dane z 2 lat lub dane rynkowe dla danego symbolu giełdowego. W razie potrzeby można również spakować wiele funkcji i logiki biznesowej.

wystarczy otworzyć połączenie przez gniazdo z serwerem i przejrzeć dane wiążące zdarzenie ze zdarzeniem. Można o tym myśleć w ten sam sposób, jak o projektowaniu klasy, tylko że nie pisze się wszędzie metod pomocniczych, które tak naprawdę działają tylko dla jednej funkcjonalności. poniżej znajduje się próbka tego, jak to może wyglądać. Zwróć uwagę, że używam zmiennych, a metody „pomocnicze” znajdują się poniżej. W Wreszcie ładnie usuwam programy obsługi zdarzeń, jeśli moja klasa Exchange byłaby zewnętrzna / wstrzyknięta, nie zarejestrowałbym żadnego oczekującego programu obsługi zdarzeń

void List<HistoricalData> RequestData(Ticker ticker, TimeSpan timeout)
{
    var socket= new Exchange(ticker);
    bool done=false;
    socket.OnData += _onData;
    socket.OnDone += _onDone;
    var request= NextRequestNr();
    var result = new List<HistoricalData>();
    var start= DateTime.Now;
    socket.RequestHistoricalData(requestId:request:days:1);
    try
    {
      while(!done)
      {   //stop when take to long….
        if((DateTime.Now-start)>timeout)
           break;
      }
      return result;

    }finally
    {
        socket.OnData-=_onData;
        socket.OnDone-= _onDone;
    }


   void _OnData(object sender, HistoricalData data)
   {
       _result.Add(data);
   }
   void _onDone(object sender, EndEventArgs args)
   {
      if(args.ReqId==request )
         done=true;
   } 
}

Możesz zobaczyć zalety, jak wspomniano poniżej, tutaj możesz zobaczyć przykładową implementację. Mam nadzieję, że pomoże to wyjaśnić korzyści.

Walter Vehoeven
źródło
2
1. To naprawdę złożony przykład i wyjaśnienie, aby zademonstrować lokalne funkcje. 2. Funkcje lokalne nie unikają alokacji w porównaniu z lambdami w tym przykładzie, ponieważ nadal muszą zostać przekonwertowane na delegatów. Więc nie widzę, jak mogliby uniknąć GC.
svick
1
bez przekazywania / kopiowania zmiennych, odpowiedź Svicka bardzo dobrze obejmuje resztę. Nie ma potrzeby powtarzania jego odpowiedzi
Walter Vehoeven