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


178

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

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

Будь-який приклад буде дуже вдячний. Дякую.


9
Загальні параметри, параметри, рекурсивні функції без необхідності ініціалізації лямбда на нуль тощо.
Кірк Уолл

5
@KirkWoll - Ви повинні опублікувати це як відповідь.
Енігмативність

Відповіді:


276

Це пояснив Мадс Торгерсен у примітках про зустрічі дизайнерів C #, де вперше обговорювались локальні функції :

Ви хочете функцію помічника. Ви використовуєте його лише в межах однієї функції, і він, ймовірно, використовує змінні та параметри типу, що знаходяться в області, що містить функцію. З іншого боку, на відміну від лямбда, він вам не потрібен як об'єкт першого класу, тому вам не байдуже надавати йому тип делегата та виділяти фактичний об’єкт делегата. Також ви можете хотіти, щоб він був рекурсивним або загальним, або реалізувати його як ітератор.

Щоб розширити на ньому ще деякі переваги:

  1. Продуктивність.

    Створюючи лямбда, слід створити делегата, що в цьому випадку є зайвим виділенням. Локальні функції - це справді лише функції, делегати не потрібні.

    Також локальні функції є більш ефективними при захопленні локальних змінних: лямбдаси зазвичай захоплюють змінні в клас, тоді як локальні функції можуть використовувати структуру (передану за допомогою ref), що знову уникає розподілу.

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

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

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

  3. Локальні функції можуть бути загальними.

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

  4. Локальні функції можна реалізувати як ітератор.

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

  5. Місцеві функції виглядають краще.

    Це не згадується у вищенаведеній цитаті, і це може бути лише моїм особистим ухилом, але я думаю, що синтаксис нормальної функції виглядає краще, ніж присвоєння лямбда-змінної змінної. Місцеві функції також більш лаконічні.

    Порівняйте:

    int add(int x, int y) => x + y;
    Func<int, int, int> add = (x, y) => x + y;
    

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 Оскільки лямбдаші завжди використовують делегата, і цей делегат має право закрити як an 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
Це дійсно корисно, оскільки я можу звикнути ставити всі допоміжні функції в a #region Helpersв нижній частині функції, щоб уникнути захаращення в рамках цієї функції та епізодично уникати безладу в основному класі.
AustinWBryan

Я також ціную це. Це робить основну функцію, яку ви дивитесь, легше читати, оскільки вам не потрібно озиратися, щоб знайти, де вона починається. Якщо ви хочете ознайомитись із деталями реалізації, продовжуйте оглядатися до кінця.
Ремі Деспрес-Сміт

3
якщо ваші функції такі великі, їм потрібні регіони, вони занадто великі.
ssmith

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 .


чудово бачити таких пристрасних розробників, які виступають за те, щоб люди могли використовувати свій інструмент. Як вас зацікавило написання інструментів для розробників як робота на повний робочий день? Як американець, моє враження таке, що важко знайти таку кар’єру, якщо у вас немає магістрів або кандидатів наук. в comp sci.
Іван Заброський

Привіт Джон, і дякую за добрі слова. Як розробник програмного забезпечення я не бачу нічого кращого, ніж оцінювати мої клієнти за цінність, яку я їм надаю. Поєднайте це з бажанням складної та конкурентоспроможної роботи, і ви отримаєте досить обмежений перелік речей, про які б я захопився. Написання інструментів для розробників продуктивності є у цьому списку. Принаймні, на мій погляд :) Щодо кар’єри, я думаю, що компанії, що надають інструменти для розробників, є досить малим відсотком від усіх програмних компаній, і саме тому знайти таку можливість важче.
Михайло Владов

Одне окреме запитання. Чому ви не зателефонуєте VerifyAll сюди? Чи є спосіб сказати JustMock, щоб перевірити, чи була викликана локальна функція?
Іван Заброський

2
Привіт @JohnZabroski, перевірений сценарій не потребував затвердження подій. Звичайно, ви могли перевірити, чи був здійснений дзвінок. Спочатку потрібно вказати, скільки разів ви очікуєте виклику методу. .DoNothing().OccursOnce();Ось так : І пізніше запевняйте, що виклик був здійснений за допомогою виклику Mock.Assert(foo);методу. Якщо вас цікавить, як підтримуються інші сценарії, ви можете ознайомитися з нашою довідковою статтею про виникнення випадків .
Михайло Владов

0

Я використовую вбудовані функції, щоб уникнути тиску збору сміття, особливо коли стосуються довших методів роботи. Скажіть, ви хочете отримати 2 роки або ринкові дані для даного символу тикера. Крім того, можна запакувати багато функціональності та ділової логіки, якщо потрібно.

що можна зробити, це відкрити сокетне з'єднання з сервером і передати цикл на дані, що прив'язують подію до події. Можна думати про це так само, як створений клас, лише один не пише усюди допоміжних методів, які справді працюють лише за одну частину функціональності. нижче наводиться приклад того, як це може виглядати, будь ласка, зауважте, що я використовую змінні, а "помічницькі" методи нижче, нарешті, нижче. Нарешті я добре видаляю обробники подій, якби мій клас 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 дуже добре висвітлює решту. Не потрібно дублювати його відповідь
Уолтер Веховен
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.