Вопрос

Я только сейчас узнаю о указателях функций, и, когда я читал главу K & R по этому вопросу, первое, что поразило меня, было: «Эй, это вроде как закрытие». Я знал, что это предположение как -то в корне неверно, и после поиска в Интернете я не нашел никакого анализа этого сравнения.

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

Почему передача функции функции считается более мощной во втором случае, когда она не имеет имени, чем в первом, где передается обычная повседневная функция?

Пожалуйста, скажите мне, как и почему я ошибаюсь, сравнивая эти два понятия так близко.

Спасибо.

Это было полезно?

Решение

Лямбда (или закрытие) инкапсулирует как указатель функции, так и переменные.Вот почему в C# вы можете сделать:

int lessThan = 100;
Func<int, bool> lessThanTest = delegate(int i) {
   return i < lessThan;
};

Я использовал там анонимный делегат в качестве замыкания (его синтаксис немного понятнее и ближе к C, чем лямбда-эквивалент), который захватывал lessThan (переменную стека) в замыкание.При оценке замыкания ссылка на lessThan (чей кадр стека мог быть уничтожен) будет продолжать ссылаться.Если я изменю lessThan, я изменю сравнение:

int lessThan = 100;
Func<int, bool> lessThanTest = delegate(int i) {
   return i < lessThan;
};

lessThanTest(99); // returns true
lessThan = 10;
lessThanTest(99); // returns false

В C это было бы незаконно:

BOOL (*lessThanTest)(int);
int lessThan = 100;

lessThanTest = &LessThan;

BOOL LessThan(int i) {
   return i < lessThan; // compile error - lessThan is not in scope
}

хотя я мог бы определить указатель на функцию, который принимает 2 аргумента:

int lessThan = 100;
BOOL (*lessThanTest)(int, int);

lessThanTest = &LessThan;
lessThanTest(99, lessThan); // returns true
lessThan = 10;
lessThanTest(100, lessThan); // returns false

BOOL LessThan(int i, int lessThan) {
   return i < lessThan;
}

Но теперь мне нужно передать два аргумента, когда я его оцениваю.Если бы я хотел передать этот указатель на функцию другой функции, где lessThan не находился в области видимости, мне пришлось бы либо вручную поддерживать его активность, передавая его каждой функции в цепочке, либо повышая его до глобального значения.

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

Краткое содержание:замыкание представляет собой комбинацию указателя функции + захваченных переменных.

Другие советы

Как человек, писавший компиляторы для языков как с «настоящими» замыканиями, так и без них, я с уважением не согласен с некоторыми ответами выше.Замыкание Lisp, Scheme, ML или Haskell не создает новую функцию динамически.Вместо этого это повторно использует существующую функцию но делает это с новые свободные переменные.Совокупность свободных переменных часто называют среда, по крайней мере, со стороны теоретиков языка программирования.

Замыкание — это просто агрегат, содержащий функцию и среду.В компиляторе Standard ML of New Jersey мы представили единицу как запись;одно поле содержало указатель на код, а другие поля содержали значения свободных переменных.Компилятор динамически создал новое замыкание (не функцию) путем выделения новой записи, содержащей указатель на такой же код, но с другой значения свободных переменных.

Вы можете смоделировать все это на C, но это заноза в заднице.Популярны две технологии:

  1. Передайте указатель на функцию (код) и отдельный указатель на свободные переменные, чтобы замыкание было разделено на две переменные C.

  2. Передайте указатель на структуру, где структура содержит значения свободных переменных, а также указатель на код.

Техника №1 идеальна, когда вы пытаетесь имитировать какой-либо полиморфизм в C, и вы не хотите раскрывать тип среды — вы используете указатель void* для представления среды.В качестве примера посмотрите книгу Дэйва Хэнсона. C-интерфейсы и реализации.Техника №2, которая больше напоминает то, что происходит в компиляторах машинного кода функциональных языков, также напоминает другую знакомую технику...Объекты C++ с виртуальными функциями-членами.Реализации практически идентичны.

Это наблюдение привело к остроте со стороны Генри Бейкера:

Люди в мире Алгола/Фортрана годами жаловались, что не понимают, какое возможное применение замыкания функций могут иметь в эффективном программировании будущего.Затем произошла революция в «объектно-ориентированном программировании», и теперь все программируют, используя замыкания функций, за исключением того, что они до сих пор отказываются называть их так.

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

Проще говоря, указатели на функции не имеют связанной с ними области действия (если только вы не учитываете глобальную область), тогда как замыкания включают область действия метода, который их определяет.С помощью лямбд-выражений вы можете написать метод, который записывает метод.Закрытие позволяет вам связывать «некоторые аргументы с функцией и получение функции более низкой ароматы» в результате ». (взято из комментария Томаса).Вы не можете сделать это в C.

РЕДАКТИРОВАТЬ:Добавляем пример (я собираюсь использовать синтаксис в стиле Actionscript, потому что именно об этом я сейчас думаю):

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

function runLater(f:Function):Void {
  sleep(100);
  f();
}

Теперь предположим, что вы хотите, чтобы пользователь runLater() задержал некоторую обработку объекта:

function objectProcessor(o:Object):Void {
  /* Do something cool with the object! */
}

function process(o:Object):Void {
  runLater(function() { objectProcessor(o); });
}

Функция, которую вы передаете в процесс(), больше не является статически определенной функцией.Он генерируется динамически и может включать ссылки на переменные, которые находились в области видимости при определении метода.Таким образом, он может получить доступ к «o» и «objectProcessor», даже если они не находятся в глобальной области видимости.

Надеюсь, это имело смысл.

Замыкание = логика + окружение.

Например, рассмотрим этот метод C# 3:

public Person FindPerson(IEnumerable<Person> people, string name)
{
    return people.Where(person => person.Name == name);
}

Лямбда-выражение инкапсулирует не только логику («сравнить имя»), но и среду, включая параметр (т. е.локальная переменная) «имя».

Подробнее об этом читайте в моем статья о замыканиях который проведет вас через C# 1, 2 и 3 и покажет, как замыкания упрощают работу.

В C указатели на функции могут передаваться в качестве аргументов функций и возвращаться в виде значений функций, но функции существуют только на верхнем уровне:вы не можете вкладывать определения функций друг в друга.Подумайте, что нужно, чтобы язык C поддерживал вложенные функции, которые могут обращаться к переменным внешней функции, сохраняя при этом возможность отправлять указатели на функции вверх и вниз по стеку вызовов.(Чтобы следовать этому объяснению, вы должны знать основы реализации вызовов функций в C и большинстве подобных языков:просмотреть стек вызовов запись в Википедии.)

Какой объект является указателем на вложенную функцию?Это не может быть просто адрес кода, потому что, если вы его вызовете, как он получит доступ к переменным внешней функции?(Помните, что из-за рекурсии одновременно может быть несколько разных вызовов внешней функции.) Это называется проблема с грибком, и есть две подзадачи:проблема нисходящихфунаргов и проблема восходящихфунаргов.

Проблема нисходящих функций, т. е. отправка указателя функции «вниз по стеку» в качестве аргумента вызываемой вами функции, на самом деле не является несовместимой с C и GCC. поддерживает вложенные функции как нисходящие функции.В GCC, когда вы создаете указатель на вложенную функцию, вы действительно получаете указатель на батут, динамически создаваемый фрагмент кода, который настраивает статический указатель ссылки а затем вызывает реальную функцию, которая использует указатель статической ссылки для доступа к переменным внешней функции.

Задача о восходящих фунаргах более сложна.GCC не запрещает вам позволять указателю трамплина существовать после того, как внешняя функция больше не активна (не имеет записи в стеке вызовов), и тогда указатель статической ссылки может указывать на мусор.Записи активации больше нельзя размещать в стеке.Обычное решение — разместить их в куче и позволить функциональному объекту, представляющему вложенную функцию, просто указывать на запись активации внешней функции.Такой объект называется закрытие.Тогда язык обычно должен поддерживать вывоз мусора чтобы записи можно было освободить, как только на них больше не будет указателей.

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

Лямбда — это аноним, динамически определенный функция.Вы просто не можете сделать это в C...что касается замыканий (или их объединения), типичный пример Лиспа будет выглядеть примерно так:

(defun get-counter (n-start +-number)
     "Returns a function that returns a number incremented
      by +-number every time it is called"
    (lambda () (setf n-start (+ +-number n-start))))

В терминах C можно сказать, что лексическая среда (стек) get-counter захватывается анонимной функцией и изменяется внутренне, как показано в следующем примере:

[1]> (defun get-counter (n-start +-number)
         "Returns a function that returns a number incremented
          by +-number every time it is called"
        (lambda () (setf n-start (+ +-number n-start))))
GET-COUNTER
[2]> (defvar x (get-counter 2 3))
X
[3]> (funcall x)
5
[4]> (funcall x)
8
[5]> (funcall x)
11
[6]> (funcall x)
14
[7]> (funcall x)
17
[8]> (funcall x)
20
[9]> 

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

Одна важная проблема с C и замыканиями заключается в том, что переменные, размещенные в стеке, будут уничтожены при выходе из текущей области видимости, независимо от того, указывало ли на них замыкание.Это приведет к ошибкам, которые возникают у людей, когда они небрежно возвращают указатели на локальные переменные.Замыкания по сути подразумевают, что все соответствующие переменные либо являются элементами с подсчетом ссылок, либо элементами, собранными в куче.

Мне неудобно приравнивать лямбду к замыканию, потому что я не уверен, что лямбды во всех языках являются замыканиями. Иногда я думаю, что лямбды - это просто локально определенные анонимные функции без привязки переменных (Python до 2.1?).

В GCC можно моделировать лямбда-функции, используя следующий макрос:

#define lambda(l_ret_type, l_arguments, l_body)       \
({                                                    \
    l_ret_type l_anonymous_functions_name l_arguments \
    l_body                                            \
    &l_anonymous_functions_name;                      \
})

Пример из источник:

qsort (array, sizeof (array) / sizeof (array[0]), sizeof (array[0]),
     lambda (int, (const void *a, const void *b),
             {
               dump ();
               printf ("Comparison %d: %d and %d\n",
                       ++ comparison, *(const int *) a, *(const int *) b);
               return *(const int *) a - *(const int *) b;
             }));

Использование этого метода, конечно, исключает возможность работы вашего приложения с другими компиляторами и, по-видимому, является «неопределенным» поведением, поэтому YMMV.

А закрытие захватывает свободные переменные в среда.Среда по-прежнему будет существовать, даже если окружающий код больше не будет активен.

Пример в Common Lisp, где MAKE-ADDER возвращает новое замыкание.

CL-USER 53 > (defun make-adder (start delta) (lambda () (incf start delta)))
MAKE-ADDER

CL-USER 54 > (compile *)
MAKE-ADDER
NIL
NIL

Используя вышеуказанную функцию:

CL-USER 55 > (let ((adder1 (make-adder 0 10))
                   (adder2 (make-adder 17 20)))
               (print (funcall adder1))
               (print (funcall adder1))
               (print (funcall adder1))
               (print (funcall adder1))
               (print (funcall adder2))
               (print (funcall adder2))
               (print (funcall adder2))
               (print (funcall adder1))
               (print (funcall adder1))
               (describe adder1)
               (describe adder2)
               (values))

10 
20 
30 
40 
37 
57 
77 
50 
60 
#<Closure 1 subfunction of MAKE-ADDER 4060001ED4> is a CLOSURE
Function         #<Function 1 subfunction of MAKE-ADDER 4060001CAC>
Environment      #(60 10)
#<Closure 1 subfunction of MAKE-ADDER 4060001EFC> is a CLOSURE
Function         #<Function 1 subfunction of MAKE-ADDER 4060001CAC>
Environment      #(77 20)

Обратите внимание, что DESCRIBE функция показывает, что функциональные объекты для обоих закрытия одинаковы, но среда отличается.

Common Lisp делает как замыкания, так и чистые функциональные объекты (без окружения) функции и оба можно вызвать одинаково, здесь используя FUNCALL.

Основное различие возникает из-за отсутствия лексического охвата в C.

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

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

Несколько замыканий могут совместно использовать некоторые переменные, а также интерфейс объекта (в смысле ООП).чтобы сделать это в C, вам нужно связать структуру с таблицей указателей функций (это то, что делает C++, с vtable класса).

Короче говоря, замыкание — это указатель на функцию ПЛЮС некоторое состояние.это конструкция более высокого уровня

В большинстве ответов указано, что замыкания требуют указателей на функции, возможно, на анонимные функции, но поскольку Марк написал замыкания могут существовать с именованными функциями.Вот пример на Perl:

{
    my $count;
    sub increment { return $count++ }
}

Замыкание – это среда, которая определяет $count переменная.Он доступен только для increment подпрограмму и сохраняется между вызовами.

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top