Es gibt bereits viele fantastische Antworten auf diese Frage im Internet. Ich werde eine Zusammenstellung mehrerer Erklärungen und Beispiele schreiben, die ich zu diesem Thema gesammelt habe, für den Fall, dass jemand es hilfreich findet
EINFÜHRUNG
Call-by-Value (CBV)
In der Regel sind Parameter für Funktionen Call-by-Value-Parameter. Das heißt, die Parameter werden von links nach rechts ausgewertet, um ihren Wert zu bestimmen, bevor die Funktion selbst ausgewertet wird
def first(a: Int, b: Int): Int = a
first(3 + 4, 5 + 6) // will be reduced to first(7, 5 + 6), then first(7, 11), and then 7
Call-by-Name (CBN)
Aber was ist, wenn wir eine Funktion schreiben müssen, die einen Ausdruck als Parameter akzeptiert, den wir erst auswerten, wenn er in unserer Funktion aufgerufen wird? Unter diesen Umständen bietet Scala Call-by-Name-Parameter an. Dies bedeutet, dass der Parameter unverändert an die Funktion übergeben wird und seine Bewertung nach der Substitution erfolgt
def first1(a: Int, b: => Int): Int = a
first1(3 + 4, 5 + 6) // will be reduced to (3 + 4) and then to 7
Ein Call-by-Name-Mechanismus übergibt einen Codeblock an den Aufruf. Jedes Mal, wenn der Aufruf auf den Parameter zugreift, wird der Codeblock ausgeführt und der Wert berechnet. Im folgenden Beispiel wird verzögert eine Nachricht gedruckt, die zeigt, dass die Methode eingegeben wurde. Als nächstes druckt verzögert eine Nachricht mit ihrem Wert. Schließlich gibt verzögert 't' zurück:
object Demo {
def main(args: Array[String]) {
delayed(time());
}
def time() = {
println("Getting time in nano seconds")
System.nanoTime
}
def delayed( t: => Long ) = {
println("In delayed method")
println("Param: " + t)
}
}
In verzögerter Methode
Zeit in
Nanosekunden abrufen Param: 2027245119786400
Vor- und Nachteile für jeden Fall
CBN:
+ Wird häufiger beendet * siehe oben stehende Terminierung * + Hat den Vorteil, dass ein Funktionsargument nicht ausgewertet wird, wenn der entsprechende Parameter bei der Auswertung des Funktionskörpers nicht verwendet wird. - Es ist langsamer, es werden mehr Klassen erstellt (dh das Programm nimmt länger zu laden) und es verbraucht mehr Speicher.
CBV:
+ Es ist oft exponentiell effizienter als CBN, da es diese wiederholte Neuberechnung von Argumentausdrücken vermeidet, die mit Namen aufgerufen werden. Es wertet jedes Funktionsargument nur einmal aus. + Es spielt viel besser mit imperativen Effekten und Nebenwirkungen, da Sie in der Regel viel besser wissen, wann Ausdrücke ausgewertet werden. -Es kann zu einer Schleife während der Parameterauswertung führen * siehe oben stehende Terminierung *
Was ist, wenn die Kündigung nicht garantiert ist?
- Wenn die CBV-Bewertung eines Ausdrucks e endet, endet auch die CBN-Bewertung von e. - Die andere Richtung ist nicht wahr
Beispiel für die Nichtbeendigung
def first(x:Int, y:Int)=x
Betrachten Sie zuerst den Ausdruck (1, Schleife)
CBN: first (1, loop) → 1 CBV: first (1, loop) → Argumente dieses Ausdrucks reduzieren. Da es sich um eine Schleife handelt, werden die Argumente unendlich reduziert. Es endet nicht
UNTERSCHIEDE IN JEDEM FALLVERHALTEN
Definieren wir einen Methodentest, der sein wird
Def test(x:Int, y:Int) = x * x //for call-by-value
Def test(x: => Int, y: => Int) = x * x //for call-by-name
Fall1 Test (2,3)
test(2,3) → 2*2 → 4
Da wir mit bereits ausgewerteten Argumenten beginnen, werden die gleichen Schritte für Call-by-Value und Call-by-Name ausgeführt
Fall2 Test (3 + 4,8)
call-by-value: test(3+4,8) → test(7,8) → 7 * 7 → 49
call-by-name: (3+4)*(3+4) → 7 * (3+4) → 7 * 7 → 49
In diesem Fall führt Call-by-Value weniger Schritte aus
Fall3-Test (7, 2 * 4)
call-by-value: test(7, 2*4) → test(7,8) → 7 * 7 → 49
call-by-name: (7)*(7) → 49
Wir vermeiden die unnötige Berechnung des zweiten Arguments
Fall4-Test (3 + 4, 2 * 4)
call-by-value: test(7, 2*4) → test(7,8) → 7 * 7 → 49
call-by-name: (3+4)*(3+4) → 7*(3+4) → 7*7 → 49
Anderer Ansatz
Nehmen wir zunächst an, wir haben eine Funktion mit Nebeneffekt. Diese Funktion druckt etwas aus und gibt dann ein Int zurück.
def something() = {
println("calling something")
1 // return value
}
Jetzt definieren wir zwei Funktionen, die genau dieselben Int-Argumente akzeptieren, mit der Ausnahme, dass eines das Argument in einem Call-by-Value-Stil (x: Int) und das andere in einem Call-by-Name-Stil (x: => Int).
def callByValue(x: Int) = {
println("x1=" + x)
println("x2=" + x)
}
def callByName(x: => Int) = {
println("x1=" + x)
println("x2=" + x)
}
Was passiert nun, wenn wir sie mit unserer Nebenwirkungsfunktion aufrufen?
scala> callByValue(something())
calling something
x1=1
x2=1
scala> callByName(something())
calling something
x1=1
calling something
x2=1
Sie sehen also, dass in der Call-by-Value-Version der Nebeneffekt des übergebenen Funktionsaufrufs (etwas ()) nur einmal aufgetreten ist. In der Call-by-Name-Version trat der Nebeneffekt jedoch zweimal auf.
Dies liegt daran, dass Call-by-Value-Funktionen den Wert des übergebenen Ausdrucks vor dem Aufrufen der Funktion berechnen und daher jedes Mal auf denselben Wert zugegriffen wird. Call-by-Name-Funktionen berechnen jedoch bei jedem Zugriff den Wert des übergebenen Ausdrucks neu.
BEISPIELE, WO ES BESSER IST, CALL-BY-NAME ZU VERWENDEN
Von: https://stackoverflow.com/a/19036068/1773841
Einfaches Leistungsbeispiel: Protokollierung.
Stellen wir uns eine Schnittstelle wie diese vor:
trait Logger {
def info(msg: => String)
def warn(msg: => String)
def error(msg: => String)
}
Und dann so verwendet:
logger.info("Time spent on X: " + computeTimeSpent)
Wenn die info-Methode nichts unternimmt (weil beispielsweise die Protokollierungsstufe höher konfiguriert wurde), wird computeTimeSpent nie aufgerufen, was Zeit spart. Dies passiert häufig bei Protokollierern, bei denen häufig Manipulationen an Zeichenfolgen auftreten, die im Vergleich zu den zu protokollierenden Aufgaben teuer sein können.
Beispiel für die Richtigkeit: Logikoperatoren.
Sie haben wahrscheinlich Code wie diesen gesehen:
if (ref != null && ref.isSomething)
Stellen Sie sich vor, Sie würden die && Methode wie folgt deklarieren:
trait Boolean {
def &&(other: Boolean): Boolean
}
Wenn ref dann null ist, wird eine Fehlermeldung angezeigt, da isSomething bei einer Nullreferenz aufgerufen wird, bevor es an && übergeben wird. Aus diesem Grund lautet die tatsächliche Erklärung:
trait Boolean {
def &&(other: => Boolean): Boolean =
if (this) this else other
}
=> Int
ist ein anderer Typ alsInt
; Es ist "Funktion ohne Argumente, die einInt
" gegen nur erzeugenInt
. Sobald Sie über erstklassige Funktionen verfügen, müssen Sie keine Call-by-Name-Terminologie erfinden, um dies zu beschreiben.