Funktionszeiger, Closures und Lambda

Lesezeit: 10 Minuten

Benutzeravatar von None
Keiner

Ich lerne gerade etwas über Funktionszeiger und als ich das K&R-Kapitel zu diesem Thema las, war das erste, was mir auffiel: “Hey, das ist so etwas wie ein Abschluss.” Ich wusste, dass diese Annahme irgendwie grundlegend falsch ist und nach einer Suche im Internet fand ich nicht wirklich eine Analyse dieses Vergleichs.

Warum unterscheiden sich Funktionszeiger im C-Stil grundlegend von Closures oder Lambdas? Soweit ich das beurteilen kann, hat dies damit zu tun, dass der Funktionszeiger immer noch auf eine definierte (benannte) Funktion zeigt, im Gegensatz zu der Praxis, die Funktion anonym zu definieren.

Warum wird das Übergeben einer Funktion an eine Funktion im zweiten Fall, in dem sie unbenannt ist, als mächtiger angesehen als im ersten Fall, in dem nur eine normale, alltägliche Funktion übergeben wird?

Bitte sagen Sie mir, wie und warum ich falsch liege, die beiden so genau zu vergleichen.

Benutzeravatar von Mark Brackett
Markus Brackett

Ein Lambda (bzw Schließung) kapselt sowohl den Funktionszeiger als auch Variablen. Aus diesem Grund können Sie in C# Folgendes tun:

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

Ich habe dort einen anonymen Delegaten als Abschluss verwendet (seine Syntax ist etwas klarer und näher an C als das Lambda-Äquivalent), der lessThan (eine Stapelvariable) in den Abschluss einfing. Wenn der Abschluss ausgewertet wird, wird weiterhin auf lessThan (dessen Stapelrahmen möglicherweise zerstört wurde) verwiesen. Wenn ich lessThan ändere, dann ändere ich den Vergleich:

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

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

In C wäre dies illegal:

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

lessThanTest = &LessThan;

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

obwohl ich einen Funktionszeiger definieren könnte, der 2 Argumente akzeptiert:

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

Aber jetzt muss ich die 2 Argumente übergeben, wenn ich es auswerte. Wenn ich diesen Funktionszeiger an eine andere Funktion übergeben wollte, in der lessThan nicht im Gültigkeitsbereich war, musste ich ihn entweder manuell am Leben erhalten, indem ich ihn an jede Funktion in der Kette weitergab, oder indem ich ihn zu einem globalen hochstufte.

Obwohl die meisten Mainstream-Sprachen, die Closures unterstützen, anonyme Funktionen verwenden, ist dies nicht erforderlich. Sie können Closures ohne anonyme Funktionen und anonyme Funktionen ohne Closures haben.

Zusammenfassung: Eine Closure ist eine Kombination aus Funktionszeiger + erfassten Variablen.

  • Sie haben wahrscheinlich eine ältere Version von C verwendet, als Sie dies geschrieben haben, oder haben nicht daran gedacht, die Funktion weiterzuleiten, aber ich beobachte nicht das gleiche Verhalten, das Sie erwähnt haben, als ich dies getestet habe. ideone.com/JsDVBK

    – smac89

    8. November 2016 um 20:47 Uhr


  • @ smac89 – Sie haben die LessThan-Variable zu einer globalen gemacht – das habe ich ausdrücklich als Alternative erwähnt.

    – Mark Brackett

    9. November 2016 um 14:01 Uhr

  • Ihr 1. C-Snippet wird angezeigt int lessThan als global, nicht wahr? Warum sollte es also illegal sein?

    – Will Ness

    2. August um 8:16


Als jemand, der Compiler für Sprachen sowohl mit als auch ohne „echte“ Closures geschrieben hat, stimme ich einigen der obigen Antworten respektvoll nicht zu. Ein Lisp-, Scheme-, ML- oder Haskell-Abschluss erstellt keine neue Funktion dynamisch. Stattdessen es verwendet eine vorhandene Funktion wieder aber tut es mit neue freie Variablen. Die Sammlung freier Variablen wird oft als die bezeichnet Umgebungzumindest von Programmiersprachentheoretikern.

Eine Closure ist nur ein Aggregat, das eine Funktion und eine Umgebung enthält. Im Compiler Standard ML of New Jersey haben wir einen als Datensatz dargestellt; ein Feld enthielt einen Zeiger auf den Code, und die anderen Felder enthielten die Werte der freien Variablen. Der Compiler dynamisch einen neuen Verschluss (keine Funktion) erstellt durch Zuweisen eines neuen Datensatzes, der einen Zeiger auf die enthält gleich Code, aber mit anders Werte für die freien Variablen.

Sie können all dies in C simulieren, aber es ist eine Nervensäge. Zwei Techniken sind beliebt:

  1. Übergeben Sie einen Zeiger auf die Funktion (den Code) und einen separaten Zeiger auf die freien Variablen, sodass die Closure auf zwei C-Variablen aufgeteilt wird.

  2. Übergeben Sie einen Zeiger auf eine Struktur, wobei die Struktur die Werte der freien Variablen und auch einen Zeiger auf den Code enthält.

Technik Nr. 1 ist ideal, wenn Sie versuchen, etwas zu simulieren Polymorphismus in C und Sie möchten den Typ der Umgebung nicht preisgeben – Sie verwenden einen void*-Zeiger, um die Umgebung darzustellen. Schauen Sie sich zum Beispiel Dave Hanson an C-Schnittstellen und Implementierungen. Technik Nr. 2, die eher dem ähnelt, was in nativen Code-Compilern für funktionale Sprachen passiert, ähnelt auch einer anderen bekannten Technik … C++-Objekte mit virtuellen Elementfunktionen. Die Implementierungen sind nahezu identisch.

Diese Beobachtung führte zu einem Witz von Henry Baker:

Die Menschen in der Algol/Fortran-Welt haben sich jahrelang darüber beschwert, dass sie nicht verstanden haben, welchen möglichen Nutzen Funktionsabschlüsse in der effizienten Programmierung der Zukunft haben würden. Dann geschah die Revolution der objektorientierten Programmierung, und jetzt programmieren alle mit Funktionsverschlüssen, außer dass sie sich immer noch weigern, sie so zu nennen.

  • +1 für die Erklärung und das Zitat, dass OOP wirklich Schließungen sind — verwendet eine vorhandene Funktion wieder, tut dies jedoch mit neuen freien Variablen — Funktionen (Methoden), die die Umgebung (ein Strukturzeiger auf Objektinstanzdaten, die nichts als neue Zustände sind) zum Arbeiten verwenden.

    – legends2k

    26. Juni 2014 um 14:25 Uhr


Benutzeravatar von Herms
Herms

In C können Sie die Funktion nicht inline definieren, also können Sie nicht wirklich einen Abschluss erstellen. Sie geben lediglich einen Verweis auf eine vordefinierte Methode weiter. In Sprachen, die anonyme Methoden/Abschlüsse unterstützen, ist die Definition der Methoden viel flexibler.

Vereinfacht ausgedrückt ist Funktionszeigern kein Gültigkeitsbereich zugeordnet (es sei denn, Sie zählen den globalen Gültigkeitsbereich), während Closures den Gültigkeitsbereich der Methode einschließen, die sie definiert. Mit Lambdas können Sie eine Methode schreiben, die eine Methode schreibt. Closures ermöglichen es Ihnen, “einige Argumente an eine Funktion zu binden und als Ergebnis eine Funktion niedrigerer Ordnung zu erhalten”. (entnommen aus Thomas’ Kommentar). In C geht das nicht.

BEARBEITEN: Hinzufügen eines Beispiels (ich werde Actionscript-ish-Syntax verwenden, weil das gerade in meinem Kopf ist):

Angenommen, Sie haben eine Methode, die eine andere Methode als Argument verwendet, aber keine Möglichkeit bietet, Parameter an diese Methode zu übergeben, wenn sie aufgerufen wird? Wie zum Beispiel eine Methode, die eine Verzögerung verursacht, bevor die Methode ausgeführt wird, an der Sie sie übergeben haben (blödes Beispiel, aber ich möchte es einfach halten).

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

Angenommen, Sie möchten runLater() verwenden, um die Verarbeitung eines Objekts zu verzögern:

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

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

Die Funktion, die Sie an process() übergeben, ist keine statisch definierte Funktion mehr. Sie wird dynamisch generiert und kann Verweise auf Variablen enthalten, die bei der Definition der Methode im Gültigkeitsbereich waren. Es kann also auf „o“ und „objectProcessor“ zugreifen, obwohl diese nicht im globalen Gültigkeitsbereich liegen.

Ich hoffe, das hat Sinn gemacht.

  • Ich habe meine Antwort basierend auf Ihrem Kommentar angepasst. Ich bin mir immer noch nicht zu 100% über die Einzelheiten der Begriffe im Klaren, also habe ich Sie einfach direkt zitiert. 🙂

    – Hermes

    16. Oktober 2008 um 15:05 Uhr

  • Die Inline-Fähigkeit anonymer Funktionen ist ein Implementierungsdetail der (meisten?) Mainstream-Programmiersprachen – es ist keine Voraussetzung für Closures.

    – Mark Brackett

    16. Oktober 2008 um 15:16 Uhr

Verschluss = Logik + Umgebung.

Betrachten Sie beispielsweise diese C# 3-Methode:

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

Der Lambda-Ausdruck kapselt nicht nur die Logik (“Vergleiche den Namen”), sondern auch die Umgebung, einschließlich des Parameters (dh der lokalen Variablen) “Name”.

Mehr dazu finden Sie in meiner Artikel über Schließungen die Sie durch C# 1, 2 und 3 führt und zeigt, wie Closures die Dinge einfacher machen.

In C können Funktionszeiger als Argumente an Funktionen übergeben und als Werte von Funktionen zurückgegeben werden, aber Funktionen existieren nur auf der obersten Ebene: Sie können Funktionsdefinitionen nicht ineinander verschachteln. Denken Sie darüber nach, was C braucht, um verschachtelte Funktionen zu unterstützen, die auf die Variablen der äußeren Funktion zugreifen können, während es immer noch in der Lage ist, Funktionszeiger nach oben und unten im Call-Stack zu senden. (Um dieser Erklärung zu folgen, sollten Sie die Grundlagen kennen, wie Funktionsaufrufe in C und den meisten ähnlichen Sprachen implementiert werden: Blättern Sie durch die Call-Stack Eintrag bei Wikipedia.)

Was für ein Objekt ist ein Zeiger auf eine verschachtelte Funktion? Es kann nicht nur die Adresse des Codes sein, denn wenn Sie ihn aufrufen, wie greift er auf die Variablen der äußeren Funktion zu? (Denken Sie daran, dass aufgrund der Rekursion mehrere verschiedene Aufrufe der äußeren Funktion gleichzeitig aktiv sein können.) Dies wird als the bezeichnet Funarg-Problemund es gibt zwei Teilprobleme: das Abwärts-Funargs-Problem und das Aufwärts-Funargs-Problem.

Das Funargs-Problem nach unten, dh das Senden eines Funktionszeigers “den Stack hinunter” als Argument an eine aufgerufene Funktion, ist eigentlich nicht inkompatibel mit C und GCC unterstützt verschachtelte Funktionen als nach unten gerichtete Funargs. Wenn Sie in GCC einen Zeiger auf eine verschachtelte Funktion erstellen, erhalten Sie wirklich einen Zeiger auf a Trampolinein dynamisch erstelltes Codestück, das die statischer Linkzeiger und ruft dann die eigentliche Funktion auf, die den statischen Link-Zeiger verwendet, um auf die Variablen der äußeren Funktion zuzugreifen.

Das Aufwärts-Funargs-Problem ist schwieriger. GCC hindert Sie nicht daran, einen Trampolin-Zeiger existieren zu lassen, nachdem die äußere Funktion nicht mehr aktiv ist (keinen Datensatz auf der Aufrufliste hat), und dann könnte der statische Link-Zeiger auf Müll zeigen. Aktivierungsdatensätze können nicht mehr auf einem Stack zugeordnet werden. Die übliche Lösung besteht darin, sie auf dem Haufen zuzuordnen und ein Funktionsobjekt, das eine verschachtelte Funktion darstellt, einfach auf den Aktivierungsdatensatz der äußeren Funktion zeigen zu lassen. Ein solches Objekt heißt a Schließung. Dann muss die Sprache normalerweise unterstützen Müllabfuhr damit die Datensätze freigegeben werden können, sobald keine Zeiger mehr auf sie zeigen.

Lambdas (Anonyme Funktionen) sind wirklich ein separates Problem, aber normalerweise lässt eine Sprache, mit der Sie anonyme Funktionen spontan definieren können, auch zu, dass Sie sie als Funktionswerte zurückgeben, sodass sie am Ende zu Closures werden.

Ein Lambda ist ein anonymer, dynamisch definiert Funktion. Sie können das in C einfach nicht tun … was Closures (oder die Convination der beiden) betrifft, würde das typische Lisp-Beispiel ungefähr so ​​​​aussehen:

(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))))

In C-Begriffen könnte man sagen, dass die lexikalische Umgebung (der Stack) von get-counter wird von der anonymen Funktion erfasst und intern modifiziert, wie das folgende Beispiel zeigt:

[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]> 

Benutzeravatar von Andy Dent
Andy Dent

Closures implizieren, dass eine Variable vom Punkt der Funktionsdefinition an die Funktionslogik gebunden ist, wie die Möglichkeit, ein Miniobjekt im laufenden Betrieb zu deklarieren.

Ein wichtiges Problem mit C und Closures besteht darin, dass Variablen, die dem Stack zugeordnet sind, beim Verlassen des aktuellen Gültigkeitsbereichs zerstört werden, unabhängig davon, ob ein Closure auf sie zeigt. Dies würde zu der Art von Fehlern führen, die Leute bekommen, wenn sie nachlässig Zeiger auf lokale Variablen zurückgeben. Closures implizieren grundsätzlich, dass alle relevanten Variablen entweder Ref-Counted- oder Garbage Collection-Elemente auf einem Haufen sind.

Ich fühle mich nicht wohl dabei, Lambda mit Schließung gleichzusetzen, weil ich nicht sicher bin, ob Lambdas in allen Sprachen Schließungen sind. Manchmal denke ich, dass Lambdas nur lokal definierte anonyme Funktionen ohne die Bindung von Variablen waren (Python vor 2.1?).

1419730cookie-checkFunktionszeiger, Closures und Lambda

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy