Wie kann man Rückrufe in einfachem Englisch erklären? Wie unterscheiden sie sich vom Aufruf einer Funktion aus einer anderen Funktion, die einen Kontext von der aufrufenden Funktion übernimmt? Wie kann ihre Leistungsfähigkeit einem unerfahrenen Programmierer erklärt werden?
Antworten
Zu viele Anzeigen?Beispiel aus der Praxis
Hier ist ein Beispiel aus dem wirklichen Leben, nun ja, aus meinem eigenen Leben.
Als ich heute um 17 Uhr meine Arbeit beendete, hatte ich verschiedene Dinge auf meiner To-Do-Liste:
- Rufen Sie den Tierarzt an, um die Testergebnisse meines Hundes zu erhalten.
- Gehen Sie mit dem Hund spazieren.
- Arbeite an meinen Steuern.
- Spülen Sie ab.
- Beantworten Sie persönliche E-Mails.
- Wäsche waschen.
Als ich beim Tierarzt anrief, hatte ich eine Sprechstundenhilfe am Telefon. Die Sprechstundenhilfe sagte mir, ich solle warten, bis der Tierarzt verfügbar sei, damit er mir die Testergebnisse erklären könne. Die Sprechstundenhilfe wollte mich in die Warteschleife legen, bis der Tierarzt bereit war.
Wie würden Sie darauf reagieren? Ich kenne meine: wie ineffizient! Also schlug ich dem Rezeptionisten vor, dass er den Tierarzt dazu bringt, mir eine zurückrufen wenn sie bereit ist zu reden. Auf diese Weise muss ich nicht am Telefon warten, sondern kann mich anderen Aufgaben widmen. Wenn die Tierärztin dann bereit ist, kann ich meine anderen Aufgaben zurückstellen und mit ihr sprechen.
Wie es sich mit Software verhält
Ich habe nur ein Gewinde. Ich kann immer nur eine Sache auf einmal tun. Wenn ich Multi-Thread-fähig wäre, könnte ich an mehreren Aufgaben parallel arbeiten, aber leider kann ich das nicht tun.
Wenn es keine Rückrufe gäbe, würde ich, wenn ich auf eine asynchrone Aufgabe stoße, sagen Sperrung . Wenn ich z. B. beim Tierarzt anrufe, braucht die Tierärztin ca. 15 Minuten, um ihre Arbeit zu beenden, bevor sie für ein Gespräch mit mir zur Verfügung steht. Wenn es keine Rückrufe gäbe, wäre ich während dieser 15 Minuten blockiert. Ich müsste einfach nur dasitzen und warten, anstatt an meinen anderen Aufgaben zu arbeiten.
So würde der Code ohne Rückruf aussehen:
function main() {
callVet();
// blocked for 15 minutes
walkDog();
doTaxes();
doDishes();
answerPeronalEmails();
doLaundry();
}
Und jetzt mit Rückrufaktionen:
function main() {
callVet(function vetCallback(vetOnThePhoneReadyToSpeakWithMe) {
talkToVetAboutTestResults(vetOnThePhoneReadyToSpeakWithMe);
});
walkDog();
doTaxes();
doDishes();
answerPeronalEmails();
doLaundry();
}
Allgemeiner ausgedrückt: Wenn Sie sich in einer Single-Thread-Ausführungsumgebung befinden und eine asynchrone Aufgabe haben, können Sie einen Rückruf verwenden, um die Dinge in einer logischeren Reihenfolge auszuführen, anstatt diese Aufgabe Ihren einzelnen Thread blockieren zu lassen.
Ein gutes Beispiel hierfür ist ein Front-End-Code, der eine Ajax-Anfrage stellen muss. Z.B. wenn Sie ein Dashboard haben, das Informationen über einen Benutzer anzeigt. Hier ist, wie es funktionieren würde, ohne Rückrufe. Der Benutzer würde die Navigationsleiste sofort sehen, aber er müsste ein bisschen warten, um die Seitenleiste und die Fußzeile zu sehen, weil die Ajax-Anfrage getUser
dauert eine Weile (als Faustregel gilt, dass das Netz langsam ).
function main() {
displayNavbar();
const user = getUser();
// wait a few seconds for response...
displayUserDashboard(user);
displaySidebar();
displayFooter();
}
Und jetzt mit Rückrufaktionen:
function main() {
displayNavbar();
getUser(function (user) {
displayUserDashboard(user);
});
displaySidebar();
displayFooter();
}
Durch die Verwendung eines Rückrufs können wir nun die Seitenleiste und die Fußzeile anzeigen, bevor die Antwort der Ajax-Anfrage zurückkommt. Das ist so, als würde ich zur Empfangsdame sagen: "Ich möchte nicht 15 Minuten am Telefon warten. Rufen Sie mich zurück, wenn der Tierarzt bereit ist, mit mir zu sprechen, und in der Zwischenzeit werde ich an den anderen Dingen auf meiner ToDo-Liste weiterarbeiten." Im wirklichen Leben sollten Sie wahrscheinlich etwas mehr Anstand walten lassen, aber wenn Sie Software schreiben, können Sie der CPU gegenüber so unhöflich sein, wie Sie wollen.
Normalerweise haben wir Variablen an Funktionen geschickt: function1(var1, var2)
.
Nehmen wir an, Sie wollen es verarbeiten, bevor es als Argument angegeben wird: function1(var1, function2(var2))
Dies ist eine Art von Rückruf, bei der function2
führt einen Code aus und gibt eine Variable an die Ausgangsfunktion zurück.
Edit: Die häufigste Bedeutung des Wortes callback
ist eine Funktion, die als Argument an eine andere Funktion übergeben wird und zu einem späteren Zeitpunkt aufgerufen wird. Diese Ideen finden sich in Sprachen, die Funktionen höherer Ordnung zulassen, d.h. Funktionen als Bürger erster Klasse behandeln, und sie werden typischerweise verwendet in async
Programmierung. onready(dosomething)
. Hier dosomething
geschieht nur, wenn sie bereit ist.
Rückrufe lassen sich am einfachsten anhand der Telefonanlage beschreiben. Ein Funktionsanruf ist vergleichbar mit einem Telefonanruf, bei dem man jemanden anruft, ihm eine Frage stellt, eine Antwort erhält und dann auflegt. Wenn man einen Rückruf hinzufügt, ändert sich die Analogie dahingehend, dass man, nachdem man eine Frage gestellt hat, auch seinen Namen und seine Nummer angibt, damit er einen mit der Antwort zurückrufen kann. -- Paul Jakubik "Callback-Implementierungen in C++".
Ohne Rückruf weder andere spezielle Programmierung Ressourcen (wie Threading, und andere), ein Programm ist genau eine Folge von Anweisungen, die nacheinander ausgeführt werden und sogar mit einer Art "dynamischem Verhalten", das von bestimmten Bedingungen abhängt, alle möglichen Szenarien müssen vorher programmiert werden .
Wenn wir also einem Programm ein wirklich dynamisches Verhalten verleihen wollen, können wir Callback verwenden. Mit Callback kann man ein Programm über Parameter anweisen, ein anderes Programm mit vorher definierten Parametern aufzurufen und Ergebnisse zu erwarten ( dies ist der Vertrag oder die Unterschrift des Vorgangs ), so dass diese Ergebnisse von einem Drittanbieterprogramm erzeugt/verarbeitet werden können, das bisher nicht bekannt war.
Diese Technik ist die Grundlage des Polymorphismus, der auf Programme, Funktionen, Objekte und alle anderen Code-Einheiten angewandt wird, die von Computern ausgeführt werden.
Die menschliche Welt, die als Beispiel für den Rückruf verwendet wird, ist gut erklärt, wenn Sie eine Arbeit verrichten, nehmen wir an, Sie sind ein Maler ( Hier sind Sie das Hauptprogramm, das malt ) und rufen Sie Ihren Kunden manchmal an, um ihn zu bitten, das Ergebnis Ihrer Arbeit zu genehmigen, damit er entscheidet, ob das Bild gut ist ( Ihr Kunde ist das Drittanbieterprogramm ).
In dem obigen Beispiel sind Sie ein Maler und "delegieren" den Auftrag an andere, das Ergebnis zu genehmigen, das Bild ist der Parameter, und jeder neue Kunde (die zurückgerufene "Funktion") ändert das Ergebnis Ihrer Arbeit, indem er entscheidet, was er über das Bild will ( die von den Kunden getroffenen Entscheidungen sind das von der "Callback-Funktion" zurückgegebene Ergebnis ).
Ich hoffe, dass diese Erklärung nützlich sein kann.
"In der Computerprogrammierung ist ein Rückruf ein Verweis auf einen ausführbaren Code oder einen Teil eines ausführbaren Codes, der als Argument an einen anderen Code übergeben wird. Dadurch kann eine untergeordnete Softwareschicht ein Unterprogramm (oder eine Funktion) aufrufen, das in einer übergeordneten Schicht definiert ist." - Wikipedia
Rückruf in C mit Funktionszeiger
In C wird der Rückruf mit Hilfe von Funktionszeigern implementiert. Function Pointer - wie der Name schon sagt, ist ein Zeiger auf eine Funktion.
Zum Beispiel: int (*ptrFunc) ();
Hier ist ptrFunc ein Zeiger auf eine Funktion, die keine Argumente benötigt und eine ganze Zahl zurückgibt. Vergessen Sie nicht, die Klammern zu setzen, da der Compiler sonst davon ausgeht, dass ptrFunc ein normaler Funktionsname ist, der keine Argumente benötigt und einen Zeiger auf eine ganze Zahl zurückgibt.
Hier ist etwas Code, um den Funktionszeiger zu demonstrieren.
#include<stdio.h>
int func(int, int);
int main(void)
{
int result1,result2;
/* declaring a pointer to a function which takes
two int arguments and returns an integer as result */
int (*ptrFunc)(int,int);
/* assigning ptrFunc to func's address */
ptrFunc=func;
/* calling func() through explicit dereference */
result1 = (*ptrFunc)(10,20);
/* calling func() through implicit dereference */
result2 = ptrFunc(10,20);
printf("result1 = %d result2 = %d\n",result1,result2);
return 0;
}
int func(int x, int y)
{
return x+y;
}
Lassen Sie uns nun versuchen, das Konzept des Rückrufs in C mit Hilfe von Funktionszeigern zu verstehen.
Das vollständige Programm besteht aus drei Dateien: callback.c, reg_callback.h und reg_callback.c.
/* callback.c */
#include<stdio.h>
#include"reg_callback.h"
/* callback function definition goes here */
void my_callback(void)
{
printf("inside my_callback\n");
}
int main(void)
{
/* initialize function pointer to
my_callback */
callback ptr_my_callback=my_callback;
printf("This is a program demonstrating function callback\n");
/* register our callback function */
register_callback(ptr_my_callback);
printf("back inside main program\n");
return 0;
}
/* reg_callback.h */
typedef void (*callback)(void);
void register_callback(callback ptr_reg_callback);
/* reg_callback.c */
#include<stdio.h>
#include"reg_callback.h"
/* registration goes here */
void register_callback(callback ptr_reg_callback)
{
printf("inside register_callback\n");
/* calling our callback function my_callback */
(*ptr_reg_callback)();
}
Wenn wir dieses Programm ausführen, lautet die Ausgabe
Dieses Programm demonstriert die Funktion Callback innerhalb von register_callback innerhalb von my_callback zurück im Hauptprogramm
Die Funktion der höheren Schicht ruft eine Funktion der niedrigeren Schicht wie ein normaler Aufruf auf, und der Callback-Mechanismus ermöglicht es der Funktion der niedrigeren Schicht, die Funktion der höheren Schicht durch einen Zeiger auf eine Callback-Funktion aufzurufen.
Rückruf in Java mit Schnittstelle
Java kennt das Konzept des Funktionszeigers nicht Es implementiert den Callback-Mechanismus durch seinen Schnittstellenmechanismus Anstelle eines Funktionszeigers deklarieren wir hier eine Schnittstelle mit einer Methode, die aufgerufen wird, wenn der Aufrufer seine Aufgabe beendet hat
Lassen Sie es mich an einem Beispiel demonstrieren:
Die Callback-Schnittstelle
public interface Callback
{
public void notify(Result result);
}
Der Anrufer oder die Klasse der höheren Stufe
public Class Caller implements Callback
{
Callee ce = new Callee(this); //pass self to the callee
//Other functionality
//Call the Asynctask
ce.doAsynctask();
public void notify(Result result){
//Got the result after the callee has finished the task
//Can do whatever i want with the result
}
}
Der Callee oder die Funktion der unteren Schicht
public Class Callee {
Callback cb;
Callee(Callback cb){
this.cb = cb;
}
doAsynctask(){
//do the long running task
//get the result
cb.notify(result);//after the task is completed, notify the caller
}
}
Rückruf mit EventListener-Muster
- Posten auflisten
Dieses Muster wird verwendet, um 0 bis n Beobachter/Hörer darüber zu informieren, dass eine bestimmte Aufgabe beendet ist
- Posten auflisten
Der Unterschied zwischen dem Callback-Mechanismus und dem EventListener/Observer-Mechanismus besteht darin, dass der Callback den einzigen Aufrufer benachrichtigt, während der EventListener/Observer jeden benachrichtigen kann, der an dem Ereignis interessiert ist (die Benachrichtigung kann auch an andere Teile der Anwendung gehen, die die Aufgabe nicht ausgelöst haben)
Lassen Sie es mich anhand eines Beispiels erklären.
Die Ereignisschnittstelle
public interface Events {
public void clickEvent();
public void longClickEvent();
}
Klasse Widget
package com.som_itsolutions.training.java.exampleeventlistener;
import java.util.ArrayList;
import java.util.Iterator;
public class Widget implements Events{
ArrayList<OnClickEventListener> mClickEventListener = new ArrayList<OnClickEventListener>();
ArrayList<OnLongClickEventListener> mLongClickEventListener = new ArrayList<OnLongClickEventListener>();
@Override
public void clickEvent() {
// TODO Auto-generated method stub
Iterator<OnClickEventListener> it = mClickEventListener.iterator();
while(it.hasNext()){
OnClickEventListener li = it.next();
li.onClick(this);
}
}
@Override
public void longClickEvent() {
// TODO Auto-generated method stub
Iterator<OnLongClickEventListener> it = mLongClickEventListener.iterator();
while(it.hasNext()){
OnLongClickEventListener li = it.next();
li.onLongClick(this);
}
}
public interface OnClickEventListener
{
public void onClick (Widget source);
}
public interface OnLongClickEventListener
{
public void onLongClick (Widget source);
}
public void setOnClickEventListner(OnClickEventListener li){
mClickEventListener.add(li);
}
public void setOnLongClickEventListner(OnLongClickEventListener li){
mLongClickEventListener.add(li);
}
}
Klasse Schaltfläche
public class Button extends Widget{
private String mButtonText;
public Button (){
}
public String getButtonText() {
return mButtonText;
}
public void setButtonText(String buttonText) {
this.mButtonText = buttonText;
}
}
Klasse Checkbox
public class CheckBox extends Widget{
private boolean checked;
public CheckBox() {
checked = false;
}
public boolean isChecked(){
return (checked == true);
}
public void setCheck(boolean checked){
this.checked = checked;
}
}
Aktivität Klasse
package com.som_itsolutions.training.java.exampleeventlistener;
public class Activity implements Widget.OnClickEventListener
{
public Button mButton;
public CheckBox mCheckBox;
private static Activity mActivityHandler;
public static Activity getActivityHandle(){
return mActivityHandler;
}
public Activity ()
{
mActivityHandler = this;
mButton = new Button();
mButton.setOnClickEventListner(this);
mCheckBox = new CheckBox();
mCheckBox.setOnClickEventListner(this);
}
public void onClick (Widget source)
{
if(source == mButton){
mButton.setButtonText("Thank you for clicking me...");
System.out.println(((Button) mButton).getButtonText());
}
if(source == mCheckBox){
if(mCheckBox.isChecked()==false){
mCheckBox.setCheck(true);
System.out.println("The checkbox is checked...");
}
else{
mCheckBox.setCheck(false);
System.out.println("The checkbox is not checked...");
}
}
}
public void doSomeWork(Widget source){
source.clickEvent();
}
}
Andere Klasse
public class OtherClass implements Widget.OnClickEventListener{
Button mButton;
public OtherClass(){
mButton = Activity.getActivityHandle().mButton;
mButton.setOnClickEventListner(this);//interested in the click event //of the button
}
@Override
public void onClick(Widget source) {
if(source == mButton){
System.out.println("Other Class has also received the event notification...");
}
}
Hauptklasse
public class Main {
public static void main(String[] args) {
// TODO Auto-generated method stub
Activity a = new Activity();
OtherClass o = new OtherClass();
a.doSomeWork(a.mButton);
a.doSomeWork(a.mCheckBox);
}
}
Wie Sie aus dem obigen Code ersehen können, haben wir eine Schnittstelle namens events, die grundsätzlich alle Ereignisse auflistet, die in unserer Anwendung auftreten können. Die Widget-Klasse ist die Basisklasse für alle UI-Komponenten wie Schaltflächen und Kontrollkästchen. Diese UI-Komponenten sind die Objekte, die tatsächlich die Ereignisse vom Framework-Code empfangen. Die Widget-Klasse implementiert die Ereignisschnittstelle und verfügt über zwei verschachtelte Schnittstellen, nämlich OnClickEventListener & OnLongClickEventListener
Diese beiden Schnittstellen sind für das Abhören von Ereignissen zuständig, die auf den von Widget abgeleiteten UI-Komponenten wie Button oder Checkbox auftreten können. Wenn wir also dieses Beispiel mit dem früheren Callback-Beispiel mit Java Interface vergleichen, arbeiten diese beiden Schnittstellen als Callback-Schnittstelle. Der Code auf höherer Ebene (hier die Aktivität) implementiert also diese beiden Schnittstellen. Und wann immer ein Ereignis in einem Widget auftritt, wird der Code der höheren Ebene (oder die Methode dieser Schnittstellen, die im Code der höheren Ebene implementiert ist, hier also Activity) aufgerufen.
Lassen Sie mich nun den grundlegenden Unterschied zwischen Callback- und Eventlistener-Muster diskutieren. Wie wir bereits erwähnt haben, kann der Callee bei Callback nur einen einzigen Caller benachrichtigen. Aber im Falle des EventListener-Musters kann sich jeder andere Teil oder jede Klasse der Anwendung für die Ereignisse registrieren, die auf der Schaltfläche oder dem Kontrollkästchen auftreten können. Ein Beispiel für diese Art von Klasse ist die OtherClass. Wenn Sie sich den Code der OtherClass ansehen, werden Sie feststellen, dass sie sich als Listener für das ClickEvent registriert hat, das auf der in der Activity definierten Schaltfläche auftreten kann. Das Interessante daran ist, dass neben der Activity (dem Aufrufer) auch diese OtherClass benachrichtigt wird, wenn das Click-Ereignis auf der Schaltfläche eintritt.