Локальная функция против Lambda C # 7.0

178

Я смотрю на новые реализации в C # 7.0, и мне интересно, что они реализовали локальные функции, но я не могу представить сценарий, где локальная функция предпочтительнее лямбда-выражения, и в чем разница между ними.

Я понимаю, что лямбда- anonymousфункции являются функциями, в то время как локальные функции - нет, но я не могу понять сценарий реального мира, где локальная функция имеет преимущества перед лямбда-выражениями

Любой пример будет высоко ценится. Спасибо.

Sid
источник
9
Обобщения, параметры out, рекурсивные функции без необходимости инициализировать лямбда в ноль и т. Д.
Кирк Волл
5
@KirkWoll - Вы должны опубликовать это как ответ.
Загадка

Ответы:

276

Это было объяснено Мэдсом Торгерсеном из C # Design Notes, где впервые обсуждались локальные функции :

Вы хотите вспомогательную функцию. Вы используете его только из одной функции, и он, вероятно, использует переменные и параметры типа, которые находятся в области действия этой содержащей функции. С другой стороны, в отличие от лямбды, он не нужен как объект первого класса, поэтому вам не нужно придавать ему тип делегата и выделять фактический объект делегата. Также вы можете захотеть, чтобы он был рекурсивным или универсальным или реализовывать его как итератор.

Чтобы расширить это еще, преимущества:

  1. Производительность.

    При создании лямбды должен быть создан делегат, который в данном случае является ненужным. Локальные функции - это просто функции, делегаты не нужны.

    Кроме того, локальные функции более эффективны при захвате локальных переменных: лямбда-выражения обычно записывают переменные в класс, в то время как локальные функции могут использовать структуру (передается с использованием ref), что опять-таки позволяет избежать выделения.

    Это также означает, что вызов локальных функций обходится дешевле и может быть встроенным, что может еще больше повысить производительность.

  2. Локальные функции могут быть рекурсивными.

    Лямбды также могут быть рекурсивными, но для этого требуется неудобный код, в котором вы сначала назначаете nullпеременную-делегат, а затем лямбду. Локальные функции могут быть рекурсивными (в том числе взаимно рекурсивными).

  3. Локальные функции могут быть общими.

    Лямбды не могут быть универсальными, поскольку они должны быть назначены переменной с конкретным типом (этот тип может использовать универсальные переменные из внешней области видимости, но это не одно и то же).

  4. Локальные функции могут быть реализованы как итератор.

    Lambdas не может использовать ключевое слово yield returnyield break) для реализации IEnumerable<T>функции -returning. Локальные функции могут.

  5. Локальные функции выглядят лучше.

    Это не упоминается в приведенной выше цитате и может быть только моим личным уклоном, но я думаю, что нормальный синтаксис функции выглядит лучше, чем назначение лямбда-переменной для делегата. Локальные функции также более лаконичны.

    Для сравнения:

    int add(int x, int y) => x + y;
    Func<int, int, int> add = (x, y) => x + y;
svick
источник
22
Я хотел бы добавить, что локальные функции имеют имена параметров на стороне вызывающей стороны. Лямбды нет.
Lensflare
3
@Lensflare Это правда, что имена параметров лямбды не сохраняются, но это потому, что они должны быть преобразованы в делегаты, которые имеют свои собственные имена. Например: Func<int, int, int> f = (x, y) => x + y; f(arg1:1, arg2:1);.
svick
1
Отличный список! Однако я могу представить, как компилятор IL / JIT может выполнять все оптимизации, упомянутые в 1., также для делегатов, если их использование соответствует определенным правилам.
Марчин Качмарек
1
@Casebash Поскольку лямбда-выражения всегда используют делегата, и этот делегат содержит закрытие как object. Итак, лямбды могут использовать структуру, но она должна быть в штучной упаковке, так что у вас все равно будет это дополнительное распределение.
svick
1
@happybits В основном, когда вам не нужно давать ему имя, например, когда вы передаете его методу.
svick
83

В дополнение к хорошему ответу svick, есть еще одно преимущество для локальных функций:
они могут быть определены в любом месте функции, даже после returnоператора.

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;
}
Тим Полманн
источник
5
Это действительно полезно, поскольку я могу привыкнуть помещать все вспомогательные функции в #region Helpersнижнюю часть функции, чтобы избежать беспорядка в этой функции и, особенно, избежать беспорядка в основном классе.
AustinWBryan
Я также ценю это. Это делает основную функцию, которую вы смотрите, легче для чтения, так как вам не нужно оглядываться, чтобы найти, где она начинается. Если вы хотите увидеть детали реализации, продолжайте смотреть в конец.
Реми Деспрес-Смит
3
если ваши функции настолько велики, что им нужны регионы, они слишком велики.
смс
9

Если вы также задаетесь вопросом, как проверить локальную функцию, вам следует проверить JustMock, поскольку она имеет функциональные возможности для этого. Вот простой пример класса, который будет протестирован:

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

А вот как выглядит тест:

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

Вот ссылка на документацию JustMock .

Отказ от ответственности. Я один из разработчиков, отвечающих за JustMock .

Михаил Владов
источник
замечательно видеть, как такие страстные разработчики выступают за то, чтобы люди использовали их инструмент. Как вы заинтересовались написанием инструментов для разработчиков на полную ставку? Как американец, у меня сложилось впечатление, что найти такую ​​карьеру может быть сложно, если у вас нет магистра или доктора наук. в комп.
Джон Заброски
Привет Джон и спасибо за добрые слова. Как разработчик программного обеспечения, я не вижу ничего лучше, чем быть оцененным моими клиентами за ценность, которую я им предоставляю. Объедините это с желанием трудной и конкурентоспособной работы, и вы получите довольно ограниченный список вещей, которыми я был бы увлечен. Написание инструментов для разработчиков продуктивности находится в этом списке. По крайней мере, на мой взгляд :) Что касается карьеры, я думаю, что компании, предоставляющие инструменты для разработчиков, составляют довольно небольшой процент от всех компаний-разработчиков программного обеспечения, и поэтому сложнее найти такую ​​возможность.
Михаил Владов
Один отдельный вопрос. Почему бы вам не позвонить в VerifyAll здесь? Есть ли способ сказать JustMock, чтобы проверить, была ли вызвана локальная функция?
Джон Заброски
2
Привет @JohnZabroski, протестированный сценарий не требует подтверждения событий. Конечно, вы можете убедиться, что был сделан звонок. Во-первых, вам нужно указать, сколько раз вы ожидаете вызова метода. Примерно так: .DoNothing().OccursOnce();а позже утверждают, что вызов был сделан путем вызова Mock.Assert(foo);метода. Если вам интересно, как поддерживаются другие сценарии, вы можете прочитать нашу справочную статью Asserting Occurrence .
Михаил Владов
0

Я использую встроенные функции, чтобы избежать давления при сборке мусора, особенно при работе с более длительными методами. Допустим, вы хотите получить 2 года или рыночные данные для данного символа тикера. Кроме того, можно упаковать много функциональности и бизнес-логики, если это необходимо.

все, что нужно сделать - это открыть сокетное соединение с сервером и зациклить данные, привязывающие событие к событию. Можно думать об этом так же, как проектируется класс, только один не пишет повсеместно вспомогательные методы, которые действительно работают только для одной функциональности. ниже приведен пример того, как это может выглядеть, обратите внимание, что я использую переменные, а методы "helper" находятся ниже, наконец. Наконец, я удаляю обработчики событий, если мой класс Exchange будет внешним / внедренным, у меня не будет зарегистрирован ни один ожидающий обработчик событий.

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

Вы можете увидеть преимущества, как указано ниже, здесь вы можете увидеть пример реализации. Надеюсь, что это помогает объяснить преимущества.

Уолтер Веховен
источник
2
1. Это действительно сложный пример и объяснение только для демонстрации локальных функций. 2. Локальные функции не избегают какого-либо распределения по сравнению с лямбдами в этом примере, потому что они все еще должны быть преобразованы в делегаты Так что я не вижу, как они будут избегать GC.
svick
1
не передавая / не копируя переменные, ответ svick действительно хорошо охватывает все остальное. Не нужно дублировать его ответ
Уолтер Веховен