Funktionsargumente und return-Werte

!!! Please ensure, that your contribution or question is placed into the relevant section !!!
Questions about rolling stock, for example, do not belong in "Questions about the Forum". Following is perhaps the right area where your question will be better looked after:
General questions to EEP , Splines, rolling stock, Structures in EEP, landscape elements, Signalling system and controlling, designers, Europe-wide EEP meetings , Gossip
Your cooperation to keep the forum clear is appreciated.
  • Funktionsargumente:


    Man kann beim Aufruf einer Funktion Werte mitgeben.

    Das ist dann sinnvoll, wenn man mit ein und derselben Funktion verschiedene Dinge tun möchte. Wenn also beispielsweise eine Funktion auf einer bestimmten Tafel mal diese, mal jene Texte anzeigen soll.


    Dazu schreibt man beim Aufruf der Funktion das in die Klammern, was man in diesem Moment benutzen möchte.

    Also zum Beispiel Anzeige_1_Bhnstg_4("ICE", "627", "Hamburg", "13:28")

    Die Funktionsdefinition bekommt in den Klammern passende Platzhalter:

    function Anzeige_Kln_Bhnstg_4(Typ, Nummer, Ziel, Zeit)

    Und dann verwendet man diese Platzhalter in der Funktion dort, wo man die mitgelieferten Werten nutzen möchte. Also zum Beispiel so:

    LUA Source Code
    1. function Anzeige_Kln_Bhnstg_4(Typ, Nummer, Ziel, Zeit)
    2. EEPStructureSetTextureText("#123", 1, Typ)
    3. EEPStructureSetTextureText("#123", 2, Typ)
    4. EEPStructureSetTextureText("#123", 3, Nummer)
    5. EEPStructureSetTextureText("#123", 4, Nummer)
    6. EEPStructureSetTextureText("#123", 5, Ziel)
    7. EEPStructureSetTextureText("#123", 6, Ziel)
    8. EEPStructureSetTextureText("#123", 7, Zeit)
    9. EEPStructureSetTextureText("#123", 8, Zeit)
    10. end


    Diese Platzhalter verhalten sich wie Variablen, gelten aber nur innerhalb dieser Funktion.

    Man sagt: Sie haben nur lokale Gültigkeit.


    Deshalb kann ich auch für eine andere Anzeige dieselben Namen verwenden, ohne dass es zu Verwechslungen kommt:

    function Anzeige_Hnvr_Bhnstg_2(Typ, Nummer, Ziel, Zeit)

    Ich weiß, welcher Text zu welcher Anzeigetafel gehört, weil er im entsprechenden Funktionsaufruf steht. Und Lua weiß es ebenfalls, weil die Argumente nur innerhalb der Funktion existieren. Anschließend werden sie sofort wieder vergessen.


    Wenn ich mit Anzeige_Hnvr_Bhnstg_2("RE", "11", "Herrhausen", "14:05") dem Anzeiger in Hannover einen neuen Text schicke, dann bleibt der Text in Köln unverändert. Das macht den Umgang mit den Namen der Variablen einfach. Ich benötige keine kryptischen Kürzel, die für jeden Bahnsteig individuell etwas anders lauten müssen. Ich brauche auch nicht unendlich viele ähnlich aussehende Namen für die ganzen Variablen. Ich kann stattdessen einfache und gut lesbare Namen wählen.


    Argumente kann man bei Funktionsdefinition und -aufruf auch untereinander schreiben:

    LUA Source Code
    1. Anzeige_1_Bhnstg_4(
    2. "ICE",
    3. "627",
    4. "Hamburg",
    5. "13:28"
    6. )

    Wahlweise mit oder ohne Einrückungen. Ganz nach persönlichem Geschmack.

    Das erhöht die Lesbarkeit enorm, wenn man ein ganzes Dutzend Werte übergeben möchte. Es ist viel ratsamer als eine einzelne Zeile mit einer Länge von weit über 200 Zeichen!


    Wenn man die gewünschten Inhalte direkt als Argumente in den Funktionsaufruf schreibt, dann erübrigt sich damit das Anlegen zusätzlicher Variablen für die Textbausteine, die man dann als Argumente in den Funktionsaufruf schreibt.


    Der folgende Ansatz ist nicht nur unleserlich, sondern auch riskant, weil er viel zu viele Möglichkeiten für Schreibfehler enthält, die später schwer zu finden sind:

    LUA Source Code
    1. function Anzeige_Kln_Bhnstg_4(Typ_Kln_Bhnstg_4, Nummer_Kln_Bhnstg_4, Ziel_Kln_Bhnstg_4, Zeit_Kln_Bhnstg_4)
    2. -- Darstellung der Texte
    3. end
    4. Typ_Kln_Bhnstg_4 = "ICE"
    5. Nummer_Kln_Bhnstg_4 = "627"
    6. Ziel_Kln_Bhnstg_4 = "Hamburg"
    7. Zeit_Kln_Bhnstg_4 = "13:28"
    8. Anzeige_1_Bhnstg_4(Typ_Kln_Bhnstg_4, Nummer_Kln_Bhnstg_4, Ziel_Kln_Bhnstg_4, Zeit_Kln_Bhnstg_4)

    Und das Beispiel enthält noch einen weiteren, eklatanten Fehler.

    Es vermischt globale und lokale Variablen kunterbunt.

    Das ist nicht anfängerfreundlich, sondern ein Anfängerfehler!

    Bitte nicht nachmachen!

    • Die Variable Typ_Kln_Bhnstg_4 in Zeile 5 ist eine globale Variable.
    • Beim Aufruf in Zeile 10 wird der Inhalt dieser globalen Variable übergeben.
    • Die Funktionsdefinition in Zeile 1 überträgt den Wert in eine neue, lokale Variable.
    • Diese lokale Variable hat denselben Namen Typ_Kln_Bhnstg_4

    Daher steht die globale Variable innerhalb dieser Funktion nicht zur Verfügung.


    Es bringt keinerlei Gewinn, die Texte zuerst globalen Variablen zuzuordnen und dann diese Variablen statt der Texte als Argumente mitzugeben.

    (Es sei denn, dass man irrtümlich der Meinung ist, man könne die einzelnen Elemente nicht untereinander schreiben.)


    Das Durcheinander von globalen und lokalen Variablen gleichen Namens führt zu einer ganzen Reihe von Missverständnissen und Fehlverhalten.

    Und am Ende zu lustigen Behauptungen wie:

    'bei return müssen Variablen in Klammern stehen, wenn sie Strings enthalten.'

    Diese Aussage ist falsch.


    Die Unterscheidung von globalen und lokalen Variablen ist für einen Anfänger gewiss nicht leicht. Aber umso wichtiger ist dann, dass man in Beispiel-Skripten für Anfänger sehr genau auf den richtigen Einsatz von globalen und lokalen Variablen achtet.



    return-Werte:


    Wenn eine Funktion bestimmte Werte liefern soll, dann kann sie diese Werte mit return zurück geben.

    Aber was bedeutet "zurück"? Wohin "zurück"?

    Dorthin, wo die Funktion aufgerufen wurde. Die Werte stehen dann an Stelle des Funktionsaufrufs. Genau so, als hätte man sie dort direkt hingeschrieben.


    Die folgende Funktion bildet das Quadrat einer Zahl:

    LUA Source Code
    1. function Quadrat(a)
    2. a = a * a
    3. return a
    4. end

    Ich darf das a innerhalb der Funktion ändern, weil es eine lokale Variable ist. Sobald die Funktion ihre Arbeit erledigt hat, wird a sowieso gelöscht.


    Jetzt rufe ich die Funktion mit einer Zahl als Argument auf:
    Quadrat(2)

    Dann steht an genau dieser Stelle im Code eine 4 (das Quadrat von 2)


    Aber wenn ich in meinem Programm einfach irgendwo eine 4 hinschreibe, dann nützt die mir nichts. Ich muss damit etwas tun.


    Ich kann sie beispielsweise in einer neuen Variable speichern:

    x = Quadrat(2)

    ist dasselbe wie

    x = 4


    Oder ich kann den Wert ausgeben

    print(Quadrat(2))

    Das ist dann dasselbe wie print(4)


    Ich kann die Funktion auch als Wert einsetzen:

    EEPSetSignal(1, Quadrat(2))

    Das setzt Signal 1 auf Stellung 4

    Ebenso kann ich eine Funktion schreiben, die durch einen Zug ausgelöst wird und dann ein paar Texte zurückgibt:

    LUA Source Code
    1. function ICE_662()
    2. return "ICE", "662", "Kassel", "15:47"
    3. end

    Dann muss ich dort, wo diese Funktion aufgerufen wird, entsprechend viele Platzhalter bereithalten, um die Werte alle aufzunehmen:

    a, b, c, d = ICE_662()

    Das Prinzip ist von vielen EEP Funktionen bekannt:

    ok, Geschwindigkeit = EEPGetTrainSpeed( "#VT98;001" ) 

    oder

    ok, Pos_X, Pos_Y, Pos_Z = EEPStructureGetPosition("#1")



    Schlusswort:


    Das ist alles immer noch "von hinten durch die Brust ins Auge" programmiert und nicht wirklich vernünftig.

    Wenn man beim Beschriften von ZZA auf Tabellen verzichtet, dann bereitet man sich damit - auch als Anfänger - mehr Probleme, als man umgeht!


    Dieser kleine Aufsatz hatte nicht das Ziel, den richtigen Weg aufzuzeigen. Vielmehr lag mir sehr dran, mit ein paar Missverständnissen aufzuräumen. Deshalb bin ich sehr nah am Original geblieben. Das fiel mir schwer, weil die ganze Struktur unklug und fehlerträchtig ist. Aber ich wollte mich unbedingt darauf konzentrieren, ein paar Falschaussagen richtig zu stellen.


    Viele Grüße

    Götz

  • Lieber Goetz ,


    Danke für die Erläuterungen. Kurze Fragee noch: Wenn ich z. B. Über einen KP den Zugnamen auslese, ist der Zugname dann auch eine lokale Variable?

    LG
    Schorsch


    -------------------------------------------------------------------------------------------------------------------------
    PC: 64-Bit, Windows 10 Home, Intel(R) Core (TM) i7-8700 K CPU@3,70 GHz, Arbeitsspeicher 32 GB, NVIDIA GeForce GTX 1080 Ti

    Laptop: acer 64 Bit, Win 10, 32 GB RAM, Intel(R) Core (TM) i7-6820 HK 2.7 GHz, NVIDIA GeForce GTX 980
    EEP11 bis EEP 15.1 Expert mit Plugin 1, EEP 16 + Patch1, 2 und 3, Modelexplorer, Zugexplorer, Modellkatalog

  • Wenn ich z. B. Über einen KP den Zugnamen auslese, ist der Zugname dann auch eine lokale Variable?

    Ja, die Variable, welche du in deiner Funktion für diesen Kontaktpunkt bereithältst, ist lokal.

    Gut erkannt :be_1:


    Du kannst sie also wahlweise nur an Ort und Stelle verwenden, um innerhalb der Funktion über diesen Namen weitere Zugdetails zu ermitteln. Oder du kannst den Namen an einen anderen Speicher (eine andere Variable oder eine Tabelle) übergeben, der dann auch ein globaler Speicher sein kann:


    LUA Source Code
    1. function Gleis_3(zugName)
    2. Zug_Gleis_3 = zugName
    3. end


    zugName ist in diesem Beispiel eine lokale Variable.

    Zug_Gleis_3 ist eine globale Variable.


    Lua unterscheidet sich leider in folgendem Punkt von anderen Programmiersprachen:


    Wenn man in Lua eine Variable in einer Funktion anlegt, dann ist sie global definiert.

    Man muss sie mit dem Schlüsselwort local als lokal deklarieren, wenn man das möchte.

    Deshalb ist Zug_Gleis_3 im obigen Beispiel eine globale Variable


    In anderen Programmiersprachen gelten Variablen, die innerhalb einer Funktion angelegt werden, nur lokal und man muss sie bei Bedarf als global deklarieren.

  • In anderen Programmiersprachen gelten Variablen, die innerhalb einer Funktion angelegt werden, nur lokal und man muss sie bei Bedarf als global deklarieren.

    Dann aber in der Regel ausserhalb der Funktion. (Jaja. Es gibt viele Ausnahmen. Nicht nur Lua.)


    :aq_1:Gruss Jürg

    Samsung Series 9 Laptop / Lenovo Z50 - 70 Laptop
    Intel i5 1.7 Ghz / Intel i7 2.0 Ghz
    4 Gb Speicher / 8 Gb Speicher
    Intel HD Graphics 4000 / NVIDIA Geforce 840M
    Windows 10 64 /Windows 10 64
    EEP16 / EEP14, EEP15
    AnlagenBau / AnlagenLaufLass

    _____________________________________________________________________

    Meinheim

  • E² = P (I.Depp)


    :aq_1:

    eep_gogo ( RG3 )

    -------------------------

    Intel i3-540 3,2GHz 8GB, RAID10, HD 6570 1GB, W7/64 Prof., EEP 6-6.1, 10-15, HN13+15, TM, "Schiefe Ebene 6 + 8", "Bahn2000", "Faszination der St. Gotthard-Nordrampe"

    MK, ModellKonverter 1.0 wo nicht mehr tut. Ganz schnell ausgeschrieben, wg. Mod-Drohung: Ansonsten ... :am_1:

  • Quote

    Lua Skripte beginnen immer mit I = 0 (fast immer)


    Das I = 0 ist wichtig, denn über das I wird im Endeffekt das Ganze gesteuert.

    Das heißt: Es muss irgendwo eine Variable durchlaufen, dass er was tun kann.

    Uijuijui

    Daran stimmt tatsächlich nichts. Überhaupt nichts!


    Das Standardskript von EEP enthält eine Variable I. Diese wird zu Beginn auf Null gesetzt und dann bei jedem Aufruf der EEPMain() Funktion hochgezählt. Mehr nicht.


    Die Variable nimmt überhaupt keinen Einfluss auf den Ablauf des Programms. Das ist nur ein Zähler. Den kann man bei Bedarf nutzen um nachzuschauen, wie oft die EEPMain() Funktion seit dem Start des Skripts schon aufgerufen wurde.


    Wenn die Anzahl der Aufrufe ohne Interesse ist, dann kann man I einfach weglassen.


    Solch ein Zähler ist aber nützlich. Denn die Funktion EEPMain() wird in sehr regelmäßigen, kurzen Abständen aufgerufen. Fünf Mal je Sekunde. Und somit bietet dieser Zähler einen zuverlässigen Zeitmesser.


    Und mit einem kleinen, aber pfiffigen Trick kann man aus dem Zähler auch noch einen regelmäßigen Takt ableiten.


    Also:

    Wenn ich den Wert von I durch 5 teile, dann weiß ich, wie viele Sekunden seit Start des Skripts vergangen sind.

    Teile ich den Wert durch 5*60, also durch 300, dann erhalte ich die vergangenen Minuten.

    Entsprechend muss ich durch 18000 (5*60*60) teilen, um die vergangenen Stunden zu erhalten.

    Und das kann man noch viel weiter treiben.


    Denn Lua kann mit sehr großen Zahlen umgehen. Dieser Zähler reicht für sämtliche EEPMain() Durchläufe in 11 Jahren! Ohne Unterbrechung oder Neustart.



    Es gibt zwei typische Anwendungen für diesen Zähler.

    Die erste ist eine Zeitmessung. Die zweite ist eine getaktete Aktion.


    Zeitmesser:


    Wenn ich möchte, dass 10 Sekunden nach einem bestimmten Ereignis etwas passiert, dann lese ich beim Ereignis den aktuellen Stand des Zählers aus und addiere die erwartete Anzahl EEPMain() Durchläufe für 10 Sekunden hinzu. Diesen Wert merke ich mir in einer neuen Variablen (oder an anderer Stelle.)


    Zeit_zu_handeln = I + 50


    Ab jetzt vergleiche ich in jedem Durchlauf, ob I den Wert erreicht hat, der in meiner Zeit_zu_handeln steht.


    LUA Source Code
    1. if I == Zeit_zu_handeln then
    2.     — tue jetzt, was getan werden muss
    3. end


    Wenn I ständig weitergezählt wird, eignet es sich als Referenz für beliebig viele zeitverzögerte Aktionen. Man merkt sich für jede der Aktionen den Wert, den I nach Ablauf der Wartezeit erreicht haben wird. Und vergleicht ständig alle gemerkten Werte mit dem fortlaufenden Zähler um zu entscheiden, ob die ausstehenden Aktionen jetzt ausgeführt werden müssen.


    LUA Source Code
    1. if I == Gleis_1_Ausfahrt then
    2.     EEPSetSignal()
    3. end
    4. if I == Gleis_2_Ausfahrt then
    5.     EEPSetSignal()
    6. end


    Das ist nicht gut organisiert. Es zeigt zunächst nur, dass ein Zähler genügt.




    Takt:


    Ein zweiter Einsatz für den Zähler in der EEPMain() Funktion ist eine getakteten Aktion.

    Es ist nicht notwendig und auch nicht sinnvoll, dafür eigene Zähler einzurichten und diese immer wieder nach Ablauf auf Null zu setzen.


    Eine Möglichkeit wäre, dass man den Vergleichswert jedesmal bei Erreichen um den gewünschten Offset rauf setzt.


    Eine zweite, viel schönere Möglichkeit bietet der Modulo Operator. Dieser Operator liefert den Rest einer Division. Wenn ich eine beliebig große Zahl durch eine zweite teile, dann ist der Rest immer kleiner als diese zweite Zahl.


    63 : 5 = 12 Rest 3

    63 % 5 = 3


    Also kann man den Zähler ständig weiterlaufen lassen.

    Möchte man alle 5 Sekunden etwas ausführen, dann teilt man die Zahl durch 25 (je 5 Durchläufe in 5 Sekunden) und betrachtet den Restwert.

    Der ist alle 5 Sekunden gleich.


    50 % 25 ist 0

    75 % 25 ist auch wieder 0

    100 % 25 ebenfalls. Und so weiter


    Gleichzeitig kann ich prüfen, ob der selbe Zähler geteilt durch 100 (für 20 Sekunden) ebenfalls 0 ergibt. Für alles, was im Abstand von 20 Sekunden passieren soll

    Und ich muss nicht zwingend prüfen, ob der Rest 0 ist. Ich kann auch andere Werte benutzen:


    Den Rest 25 gibt es nicht. Denn wenn I glatt durch 25 teilbar ist, dann ist der Rest 0


    Das letzte Code-Beispiel hat noch einen Schönheitsfehler, den ich gerne entfernen möchte. Es ist nämlich unsinnig die selbe Berechnung mehrfach auszuführen. Wenn ich das Ergebnis mehrfach benötige, dann lege ich es in einen Zwischenspeicher:


    Wenn man Aktionen nur alle paar Sekunden einmal ausführt, dann ist das keine Entlastung für EEP. Denn es passiert trotzdem genauso viel auf einmal. Nur nicht so oft.

    Wenn man wirklich die Last verringern möchte, dann verteilt man die Arbeit:


    Nach zwei Sekunden ist alles erledigt, aber in jedem Durchgang hat Lua nur ein Zehntel der Arbeit. Damit ist viel mehr gewonnen, als wenn man alle 10 Sekunden alles auf einmal abarbeitet.


    Das ist so noch zu umständlich. Wie schon zuvor gesagt, erledigt man solche Aufgaben mit Hilfe von Tabellen. Dann benötigt man keine langen if-Verzweigungen, sondern hat mit dem errechneten Modulo-Wert gleich die Adresse für die zugehörige Aktion.


    Aber so untereinander geschrieben verdeutlich es vielleicht besser, wie man mit dem Zähler I und dem Modulo Operator % die Last tatsächlich verteilen kann.



    Schlusssatz:


    Ein I = 0 sowie das zugehörige I = I + 1 in der Main-Schleife ist nicht notwendig, aber oft sinnvoll.

    Ein T = 0 nebst T = T + 1 zusätzlich neben der Variablen I ist völlig unnötig.


    Und ganz besonders lustig wird es, wenn dann im übrigen Code das I ignoriert und nur das T benutzt wird.

  • Mein local - dein local


    Ich hatte im ersten Posting dieses Threads von globalen und lokalen Variablen gesprochen.

    Aber ich habe bislang nicht erklärt, was es damit auf sich hat.


    Diese Unterscheidung gibt es in allen Programmiersprachen und sie erfüllt immer den selben Zweck. Sie hilft, die ganzen gespeicherten Daten besser zu verwalten.


    Die wichtigste Unterscheidung ist, dass es kurzlebige und langlebige Informationen gibt.


    Die Anzahl der Züge in meinem Bahnhof ändert sich ab und zu - immer dann, wenn ein Zug kommt oder geht - aber es ist ein Wert, den es ständig gibt. Und den ich vielleicht jederzeit abfragen möchte. Das wäre eine langlebige Variable. Es kann sinnvoll sein, diesen Wert ständig für Abfragen bereitzuhalten.


    Die Nummer des freien Gleises, welches ich gerade gefunden habe, ist kurzlebig. Diese Nummer benötige ich einmal. Danach ist sie wertlos. Es wäre unsinnig, diesen Wert zu behalten, nachdem man ihn "benutzt" hat. Denn wenn ich einen Zug in dieses Gleis schicke, dann ist es nicht mehr frei.



    Neben der Lebensdauer ist auch die Verfügbarkeit einer Variablen von Bedeutung.


    Wie viele Züge derzeit im Bahnhof sind, kann in vielen verschiedenen Zusammenhängen von Interesse sein. Bei der Depot-Ausfahrt. (Ist noch genug Platz?). Bei der Anzeige im Stellpult. Bei der Anzahl Taxis, die auf dem Halteplatz stehen sollen …


    Das freie Gleis interessiert hingegen nur dort, wo man es gesucht hat: In der Funktion, die den Weg für den einfahrenden Zug auswählt.



    Daher werden zwei Typen von Variablen (und ebenso Tabellen) unterschieden:


    global - überall und immer verfügbar, nachdem sie angelegt wurden

    local - nur dort verfügbar, wo sie erzeugt wurden und nur so lange, bis der Programmablauf diesen Bereich verlässt.


    Diese Unterscheidung hat mehrere Gründe.

    Der (ursprünglich) wichtigste: Platzersparnis. Speicher war begrenzt und deshalb sollte er nicht mit Informationen gefüllt sein, die nicht mehr benötigt werden.


    Heute wichtiger: Organisation der Namen. Jede globale Variable benötigt einen Namen, der im gesamten Skript einmalig ist. Und bei der Vielzahl benötigter Variablen ist das schnell problematisch. Eine lokale Variable benötigt einen Namen, der in ihrem Umfeld einmalig ist. An anderer Stelle darf ein identischer Name für eine ebenfalls lokale Variable verwendet werden. Die Programmiersprache hält beides sicher auseinander.


    Ein dritter Punkt: Die CPU hat Zugriff auf verschiedene Speicher. Zum Einen die vielen Gigabyte RAM, die heutzutage im PC stecken. Und zum Anderen einen viel kleineren Speicher direkt in der CPU - den Cache.


    Der Abruf von Daten aus dem RAM dauert länger. Weil die Adressen so lang sind. Und die Wege zum RAM auch. Der kleine Speicher, welcher innerhalb der CPU sitzt, ist viel schneller verfügbar. Dieser Speicher ist für die kurzlebigen Daten gedacht. Für Werte, die man ausrechnet, benutzt und dann vergisst.


    RAM ist gut für Daten, die selten geschrieben, aber oft abgerufen werden. Und die lange erhalten bleiben müssen. Deshalb wird RAM für die globalen Variablen benutzt.


    Der Cache ist gut für den Sofortverzehr: Ausrechnen, benutzen, weg damit. Und das passt zu einem sehr großen Teil der Daten, die in einem Programm verwendet werden.



    Bitte nicht zu sehr an meinen Beispielen aufhängen. Es kann Gründe geben die Anzahl Züge im Bahnhof nur lokal für einen einzigen Zweck zu speichern. Und ebenso kann es erforderlich sein, das freie Gleis auch über die Fahrtfreigabe hinaus zu speichern. Je nachdem, was man genau mit seinem Skript so anstellen möchte.



    Aber es ist wichtig, dass man das unterschiedliche Verhalten von lokalen und globalen Variablen versteht.


    LUA Source Code
    1. function TestFunktion()
    2. Text_1 = "Hallo" -- Variable ist überall verfügbar
    3. local Test_2 = "EEP" -- Variable nur innerhalb der Funktion definiert
    4. end
    5. TestFunktion() -- Funktionsausführung, Variablen definiert und initialisiert
    6. print(Test_1) -- gibt das Wort "Hallo" aus
    7. print(Test_2) -- Fehlermeldung, Test_2 hat keinen Inhalt - nil value


    Wenn man lokale Werte direkt nach dem Ende der Funktion einmal außerhalb verwenden möchte, dann kann die Funktion sie mit return an denjenigen übergeben, der die Funktion aufgerufen hat:

    LUA Source Code
    1. function Gruss()
    2. local Text_1 = "Hallo"
    3. local Leerzeichen = " "
    4. local Text_2 = "EEP!"
    5. return Text_1, Leerzeichen, Text2
    6. end
    7. print(Gruss())


    Man muss (und darf!) Variablen nur einmal bei der Definition als local deklarieren!

    LUA Source Code
    1. function Gruss()
    2. local Text = "Hallo"
    3. Text = Text.." "
    4. Text = Text.."EEP!"
    5. return Text
    6. end



    Schlussbemerkung:


    Argumente in Funktionsaufrufen und in Schleifenbedingungen sind automatisch lokale Variablen.

    Variablen, die direkt deklariert werden, sind automatisch globale Variablen.


    Man kann deshalb nicht entscheiden, dass man "erst einmal alles global" oder "alles lokal" macht, um es einfacher zu haben. Wer nicht weiß, wo welcher Typ entsteht und was ihn auszeichnet, der bekommt beides durcheinander, ob er will oder nicht.

    "Im weiteren Verlauf der Befassung mit diesem Thema"

    - Klaus S. -

    The post was edited 3 times, last by Goetz ().

  • Resteverwertung


    Der Modulo Operator % wird von Amateuren gerne als "zu schwer" eingestuft.

    Völlig zu Unrecht. Der Umgang damit ist kinderleicht. Auch für Anfänger!

    Und man macht sich das Leben unnötig schwer, wenn man darauf verzichtet.


    Beispielsweise möchte man die Wartezeit bis zum nächsten Zug errechnen:


    Wenn der Zug immer um "Viertel vor" abfährt und es ist jetzt erst "Zehn nach", dann kann man die aktuellen Minuten von der Abfahrtszeit abziehen, um die verbleibende Wartezeit zu bekommen:


    LUA Source Code
    1. Wartezeit = 45 - 10 -- Wartezeit beträgt 35 Minuten


    Wenn die aktuelle Uhrzeit "Fünf vor" ist, dann kann man dasselbe Prinzip anwenden, aber es kommt eine negative Zahl heraus:


    LUA Source Code
    1. Wartezeit = 45 - 55 -- Wartezeit beträgt minus 10 Minuten.


    Nun kann man prüfen, ob die errechnete Wartezeit negativ ist und in dem Fall 60 Minuten hinzuzählen, um so die richtige Wartezeit zu bekommen:

    LUA Source Code
    1. Wartezeit = 45 - 55
    2. if Wartezeit < 0 then
    3. Wartezeit = Wartezeit + 60 -- Wartezeit beträgt jetzt minus 10 plus 60 = 50 Minuten
    4. end


    Oder man wendet Modulo an:

    LUA Source Code
    1. Wartezeit = (45 - 55) % 60 -- fertig!

    Die Klammer um die Subtraktion ist notwendig, weil der Modulo Operator Vorrang vor der Strichrechnung hat. (So, wie auch Punkt- vor Strichrechnung gilt.)



    Modulo liefert den positiven Rest einer Division:


    7 geteilt durch 3 ist 2 Rest 1

    deshalb ist

    7 % 3 == 1


    45 geteilt durch 60 ist 0 Rest 45

    Also ist

    45 % 60 == 45


    105 geteilt durch 60 ist 1 Rest 45

    Deshalb ist auch

    105 % 60 == 45


    Man könnte sagen, dass -15 geteilt durch 60 gleich minus 1 Rest 45 ist.

    Deshalb ist auch

    -15 % 60 == 45

    Denn wenn ich zu (minus 1 mal 60) die 45 hinzuzähle, dann bleiben -15 übrig.


    Aber das ist vielleicht zu abstrakt gedacht?

    Dann stellt es euch auf einem Zahlenstrahl vor:



    Die Zahlen auf den grünen Pfeilen oben sind die Modulo 60 Werte, die Zahlen unter dem Zahlenstrahl die ursprüngliche Zahl.



    Also kann man mit der folgenden Formel immer die richtige Wartezeit berechnen, egal ob gerade die aktuelle Minute die größere Zahl ist oder die Minute der Abfahrtszeit:

    LUA Source Code
    1. Wartezeit = (AbfahrtMinute - EEPTimeM) % 60

    Funktioniert in dieser Form natürlich nur bei Wartezeiten unter einer Stunde - wie bei der S-Bahn.


    Die Modulo Methode ist einfacher, überschaubarer und stellt auch den Anfänger nicht vor unüberwindbare Schwierigkeiten. Im Gegenteil: Ein if-Konstrukt, welches das erste Ergebnis prüft und dann unter bestimmten Bedingungen noch einmal ändert, ist für den Anfänger viel rätselhafter.

    "Im weiteren Verlauf der Befassung mit diesem Thema"

    - Klaus S. -

    The post was edited 1 time, last by Goetz ().

  • Ein zweiter Lokaltermin

    Können wir da nochmal drüber reden? Ich meine diese Geschichte mit den globalen und den lokalen Werten? Und wo wann was landet? Und was man deshalb mit den Variablennamen lieber nicht anstellen sollte?


    Da wird an anderer Stelle etwas zusammengebraut und als "leichte Kost" verkauft, was schwer verdaulich ist und zu unschönen Reaktionen führen kann.


    Ich habe hier ein kleines Beispielskript. Das soll mir helfen die Zusammenhänge anschaulicher zu machen:

    Ihr werdet sehen, dass in den Variablen Var_1, Var_2 und Var_3 nicht immer das steht, was man erwarten würde wenn man die Unterschiede von global und local außer Acht lässt.

    Beim ersten Versuch scheint soweit alles glatt zu gehen.



    Im Ausgabefenster steht "Global_1", "Global_2" und "Global_3".

    Also genau das, was in der ersten Funktion in den drei Variablen Var_1, Var_2 und Var_3 gespeichert und dann an die zweite Funktion übergeben wurde. Perfekt!


    Oder doch nicht? Müsste da nicht vielleicht etwas ganz anderes stehen?

    Was steht denn da in der Funktion Test_2 am Anfang?

    Der Aufruf einer dritten Funktion namens Test_3().


    Und was macht die?




    Sie ändert den Text in Var_1. Der lautet jetzt "ganz neues Ding 1". Warum wird dieser Text nicht im print() in Zeile 17 ausgegeben? Sondern immer noch "Global 1"?


    Und was ist mit dem Aufruf der Funktion Test_4? Die ändert den Text in einer Var_2 auf "zurückgegebenes Ding 2". Der Text wird aber auch nicht ausgegeben. Obwohl er doch mit return an die Stelle zurückgegeben wurde, wo man die Funktion aufgerufen hat.


    Des Rätsels Lösung:

    In der Funktion Test_1 werden drei globale Variablen angelegt. Beim Aufruf der Funktion Test_2 werden nur die Werte aus diesen Variablen an drei neue, lokale Variablen übergeben. Dass diese lokalen Variablen denselben Namen haben, führt zu gedanklichen Verwechslungen. Denn innerhalb von Test_2() stehen die Namen jetzt für die lokalen Variablen. Außerhalb stehen die selben Namen für ganz andere, globale Variablen.


    Die Funktion Test_3 ändert den Inhalt der globalen Variable Var_1

    Die Funktion Test_4 ändert den Wert einer neuen, lokalen Variable Var_2 und gibt ihn zurück. Aber dort, wo die Funktion aufgerufen wurde, ist niemand um den zurückgegebenen Wert in Empfang zu nehmen. Der fällt also unter den Tisch.

    Die print() Ausgaben nutzen die lokalen Variablen Var_1, Var_2 und Var_3. Die haben zwar denselben Namen, sind aber ganz andere Speicherplätze. Die globale Variable Var_1 und die lokale Variable Var_1 haben nichts, wirklich gar nichts mit einander zu tun.


    Der Versuch 2 zeigt, dass man Test_2 auch direkt mit den gewünschten Werten aufrufen kann. Man muss die nicht erst in Variablen zwischenspeichern. Die Variablen braucht es nur in der Funtionsdefinition.



    Versuch 3 und Versuch 4 zeigen, dass alle Werte durchaus existieren.


    Im Versuch 3 wird der Inhalt der drei globalen Variablen ausgegeben. Und weil die Funktion Test_2 zuvor schon ausgeführt wurde, hat sich der Inhalt der globalen Variable Var_1 auch tatsächlich schon geändert. Die globale Variable Var_2 wurde hingegen nicht geändert. Sie hat mit der lokalen Variable des selben Namens in der Funktion Test_4 nichts zu tun.


    Der Versuch 2 hatte keinen Einfluss auf die globalen Variablen. Weil im Versuch 2 nur Texte an die lokalen Variablen von Test_2 übergeben wurden. Die wurden einmal innerhalb der Funktion benutzt und dann vergessen.


    Versuch 4 zeigt, wie man an den Rückgabewert der Funktion Test_4 kommt. Nämlich, indem man ihn beim Aufruf der Funktion an einen Empfänger übergibt. In diesem Fall an das erste Argument für print().



    Und was heißt das jetzt?

    Benutzt nicht dieselben Namen für globale und lokale Variablen. Das macht den Code nur scheinbar lesbarer. In Wirklichkeit wird er unleserlich, weil ein Name jetzt für unterschiedliche Variablen steht. Am Ende weiß man nicht mehr, wo man jetzt gerade welche von beiden Variablen nutzt oder ändert.


    Und entscheidet euch, welchen der beiden Wege ihr gehen wollt. Ihr könnt Werte weitergeben, indem ihr sie in globale Variablen schreibt. Oder, indem ihr sie als Argumente übergebt. Aber es ist völlig unsinnig, beides zugleich zu tun!


    Variante 1 (mit globalen Variablen):


    Variante 2 (mit Argumenten):


    Beide Versionen sind nicht nur sauber, sondern obendrein auch noch lesbarer als Konstrukte, in denen der selbe Variablenname viermal (mit unterschiedlicher Bedeutung) verwendet wird.


    Schlusswort:

    Mit Glück kann das Durcheinander von globalen und lokalen Variablen funktionieren. Man frickelt lange rum, kommt zwischendurch auf die lustige Idee, für return irgendwelche Variablen in Klammern zu setzen und hat eine hanebüchene Erklärung dafür, vergisst anschließend, dass diese Klammern nichts verändert hatten, lässt das aber - ebenso wie die wirkungslosen Parameter hinter dem return - einfach stehen, bläht seinen Code immer mehr auf und ist zum Schluss froh, dass er wenigstens tut, was es soll. Solange man nichts verändert.



    Vielleicht sollte ich an dieser Stelle noch einmal betonen, dass ich selbst Anfänger bin. Vor der Einführung von Lua in EEP habe ich nie programmiert. Man kann das also auch als Anfänger lernen. Leicht sogar. Man muss nur die Bereitschaft mitbringen auf diejenigen zu hören, die einem helfen möchten.


    Deshalb möchte ich mich an dieser Stelle noch einmal herzlich bei all denen bedanken, die mir immer wieder helfen und geholfen haben. Aktuell insbesondere cetz und Benny (BH2)

    Die beiden schauen mir stets auf die Finger und werden zum Glück nicht müde, mich auf meine Fehler hinzuweisen. Wenn dem nicht so wäre, dann würde ich mich nicht trauen hier Lua zu erklären. Weil ich Unsinn verbreiten würde.

    "Im weiteren Verlauf der Befassung mit diesem Thema"

    - Klaus S. -

    The post was edited 6 times, last by Goetz: Kleine Korrekturen in der Formatierung ().

  • Meine persönliche Empfehlung:

    1. Vermeidet globale Variablen ganz, wann immer es geht.
    2. Bevor Ihr doch eine globale Variable baut, geht zu Schritt 1 zurück.

    Und schon ist die Endlosschleife fertig. :ag_1:


    Im Ernst: Frickelt nicht, bis es "irgendwie" geht, sondern sucht die Fehler, behebt sie, lernt daraus und behaltet den Überblick. Sonst ist bei der nächsten Erweiterung, die schneller kommt als Ihr denkt, nämlich Schluss.


    Durch den Verzicht auf globale Variablen neigen die Funktionen und ihre Aufrufe dazu, lange Parameterlisten zu kriegen. Wenn das passiert, wäre es Zeit, sich über den angemessenen Einsatz von Lua-Tabellen Gedanken zu machen.


    Gruß

    Christopher

    PC: Intel i7-7700K; 64bit; 4,2 GHz; 16GB RAM; GeForce GTX 1080 (8 GB); Win 10; EEP 6, 13.2 Plugins 1+2, 14.1 (Dev), 15 (Dev); HomeNOS 14 (Dev)
    Laptop: Intel i5 3230M; 64bit; 2,6 GHz; 8GB RAM; GeForce GT740M (1 GB); Win 8.1; EEP 6, 13.2 Plugins 1+2; HomeNOS 13 (User)

  • Lokale Variablen beginnen bei mir immer mit einem Unterstrich

    Das kann ein guter Stil sein.

    Aber der Unterstrich macht sie nicht zur lokalen Variable. Deshalb muss man zunächst verstehen, wann Variablen lokal oder global sind, bevor man sie namentlich als solche kennzeichnen kann.


    Und mir wird gerade bewusst, dass ich das noch nicht deutlich erklärt habe. Bisher habe ich mich auf die Auswirkungen konzentriert. Ich muss also noch über ein weiteres Kapitel zum Thema nachdenken.

  • Natürlich ist das logisch, Volker. Götz weiss das und er hat Dir auch gar nicht widersprochen. Er hat nur ergänzt. Es besteht nämlich die Möglichkeit, dass Du missverstanden wirst.


    :aq_1:Gruss Jürg

    Samsung Series 9 Laptop / Lenovo Z50 - 70 Laptop
    Intel i5 1.7 Ghz / Intel i7 2.0 Ghz
    4 Gb Speicher / 8 Gb Speicher
    Intel HD Graphics 4000 / NVIDIA Geforce 840M
    Windows 10 64 /Windows 10 64
    EEP16 / EEP14, EEP15
    AnlagenBau / AnlagenLaufLass

    _____________________________________________________________________

    Meinheim

  • Hallo,

    Ein I = 0 sowie das zugehörige I = I + 1 in der Main-Schleife ist nicht notwendig, aber oft sinnvoll.

    sorry falls ich das irgendwo überlesen habe, aber kann ich I eigentlich auch im laufenden Betrieb von EEP manuell wieder auf 0 setzen oder würde diese Zuweisung von EEP nicht ausgeführt?


    Gruß Michael

    163-katalog-banner-jpg

    Hardwarekonfiguration:
    Laptop: Intel Core i3-3110M 2,4 GHz, 4GB RAM, Win7 64 bit, EEP 6.1/EEP 16.0 Expert

  • Du kannst I jederzeit auf 0 zurücksetzen.

    Dieses I existiert aber nicht in EEP, sondern nur in Lua!

    Das ist einfach der Name einer Variablen. Und Variablen heißen so, weil ihr Inhalt jederzeit änderbar - also variabel - ist.


    Du kannst, wenn du es "manuell" machen willst, ein beliebiges Signal aufstellen. Zum Beispiel einen dieser hübschen Schalter von HS1. Aber jedes andere Signal funktioniert natürlich auch.


    Und dann registrierst du dieses Signal.

    Und schreibst eine Funktion, welche die Variable auf 0 setzt, sobald dieser Schalter auf Grün geschaltet wird:

    LUA Source Code
    1. EEPRegisterSignal(123)
    2. EEPOnSignal_123(_Stellung) do
    3. if _Stellung == 2 then
    4. I = 0
    5. end
    6. end

    Aber es ist überhaupt nicht sinnvoll, das zu tun.

    Diesen Zähler lässt man am besten kontinuierlich weiterzählen. Für jeden Grund, den Zähler zurückzusetzen, gibt es eine bessere Alternative, mit der er weiterläuft. Wirklich für jeden!


    Und man gibt dieser Variablen besser einen anderen Namen als einfach nur ein großes I.

  • Hallo,

    Diesen Zähler lässt man am besten kontinuierlich weiterzählen. Für jeden Grund, den Zähler zurückzusetzen, gibt es eine bessere Alternative, mit der er weiterläuft. Wirklich für jeden!

    na der Gedanke hängt mit meiner neuen Anlage zusammen, für deren Steuerung ich noch Ideen sammle. Hierbei sollen die Züge grundsätzlich nach einer Art Fahrplan verkehren, diesen würde ich gerne (über Tabellen o.ä.) in LUA hinterlegen und dafür müsste man dann "eine Uhr" mitlaufen haben. Diese Uhr muss aber am Ende des Tages wieder auf 0 gesetzt werden. Möglicherweise ist es aber tatsächlich sinnvoller das separat zu machen, da ich die "Uhrzeit" ja beim Beenden der Anlage idealerweise auch speichern muss, da man sonst die Anlage ja nur am Ende des Tages abspeichern und beenden könnte. Diese Uhrzeit kann man ja dann mit einer einfachen IF-Bedingung bei erreichen eines bestimmten Wertes ("Tagesende") auf 0 setzen.


    Vielen Dank auf jeden Fall @all für die Antworten.


    Gruß Michael

    163-katalog-banner-jpg

    Hardwarekonfiguration:
    Laptop: Intel Core i3-3110M 2,4 GHz, 4GB RAM, Win7 64 bit, EEP 6.1/EEP 16.0 Expert

  • dafür müsste man dann "eine Uhr" mitlaufen haben

    Für diesen Zweck hast du

    EEPTime

    EEPTimeH

    EEPTimeM und

    EEPTimeS

    zur Verfügung.


    am Ende des Tages

    wäre nicht "manuell" so, wie ich es verstanden hatte.

    Also vergiss bitte meinen Vorschlag mit dem Schalter auf der Anlage.


    Natürlich kannst du ausrechnen, wie groß I am Ende des Tages wäre und es bei Erreichen dieses Wertes zurücksetzen auf 0:


    LUA Source Code
    1. function EEPMain()
    2. I = I + 1
    3. if I == 5 * 60 * 60 * 24 then
    4. I = 0
    5. end
    6. end

    Aber es ist wirklich besser, wenn du den Zähler als Zähler behältst und mit EEPTime die Anlagenzeit ausliest.

    "Im weiteren Verlauf der Befassung mit diesem Thema"

    - Klaus S. -

    The post was edited 2 times, last by Goetz ().