Objektorientierte Programmierung in LabVIEW: Die Hintergrundentscheidungen beim Entwurf

Überblick

LabVIEW: Handelt es sich um ein Entwurfswerkzeug? Oder ist es eine Programmiersprache? Es ist beides, und aus diesem Grund ist es ein großer Segen für Wissenschaftler und Ingenieure, die Computer programmieren müssen, ohne Informatiker hinzuzuziehen. Wann immer wir als LabVIEW-Entwickler neue Funktionen hinzufügen wollen, müssen wir in Betracht ziehen, dass die Mehrheit unserer Kunden keine Programmierer sind. In Version 8.2 wurde die objektorientierte Programmierung in LabVIEW (LabVIEW Object-Oriented Programming, LVOOP) eingeführt. Objektorientierung (OO) ist ein Programmierstil mit vielen abstrakten Konzepten und technischem Vokabular. Für die meisten Erläuterungen sind hierbei entweder fundierte Programmierkenntnisse oder ein langer Lernprozess erforderlich. Unser Ziel war es, diese Komplexität zu reduzieren, um die Leistungsfähigkeit von OO für ein breites Anwenderspektrum verfügbar zu machen. Das Ergebnis mag OO-Befürworter überraschen, die mit anderen Programmiersprachen vertraut sind. In diesem Dokument wird auf die Entwurfsentscheidungen und die dahinter stehenden Überlegungen bei der Erstellung von LVOOP eingegangen.

Dieses Paper setzt ein gewisses Maß an Vertrautheit mit LVOOP voraus. Bevor Sie fortfahren, sollten Sie die entsprechenden Abschnitte in der LabVIEW-Hilfe und die Beispielprogramme durchlesen.

Dieses Dokument wurde für LabVIEW 2009 aktualisiert.

Contents

Das High-Level-Design von LVOOP

Warum ist für LabVIEW eine objektorientierte Programmierung erforderlich?
Das Ziel von LabVIEW ist es, Ingenieuren und Wissenschaftlern, die über keine offizielle Programmierausbildung verfügen, die Möglichkeit zu geben, Computer zu programmieren. Wir möchten LabVIEW so strukturieren, dass sich die Benutzeroberfläche auch für Anwender, die über keine formale Programmierausbildung verfügen, intuitiv anfühlt.

Die objektorientierte Programmierung hat sich gegenüber der prozeduralen Programmierung als Architekturwahl in mehreren Programmiersprachen bewährt. Sie fördert eine klare Trennung zwischen den einzelnen Programmcodebereichen, kann einfacher auf Fehler untersucht werden und lässt sich besser auf große Programmierteams ausweiten. Die F&E von LabVIEW wollte, dass diese Möglichkeiten auch unseren Kunden zur Verfügung stehen. Wir wollten, dass die Sprache in der Lage ist, einige dieser empfohlenen Methoden in der Praxis umzusetzen.

Für die OO-Programmierung ist mehr Vorausplanung erforderlich als für die prozedurale Programmierung. Wenn Sie eine Reihe von VIs programmieren, die mehrfach verwendet werden sollen, z. B. um einen Wert zu berechnen oder einen bestimmten Wert von einer Messkarte abzurufen, dann ist es wahrscheinlich zu aufwendig, eine gesonderte Klasse zu erstellen, in der diese VIs enthalten sind. Wenn diese VIs jedoch eine Anwendung bilden, die Sie über einige Jahre hinweg pflegen wollen, ist OO möglicherweise die richtige Wahl. Wir wollten, dass LabVIEW mit den Werkzeugen des modernen Softwaredesigns ausgestattet ist, ohne dass diese Werkzeuge bei der technischen Prototypenerstellung, für die LabVIEW bekannt ist, im Weg stehen.

Die Passagen dieses Dokuments, die für LabVIEW 2009 aktualisiert wurden, sind im Text mit [LV2009] gekennzeichnet.

 

Für diejenigen, die Objektdesigns verwenden,
wollen wir, dass Verbindungen und Knoten
auf natürliche Weise
in Klassen und Methoden übergehen.


Auf welche Definition des Begriffs „objektorientierte Programmierung“ hatten wir uns geeinigt?
C++ ist eine objektorientierte Sprache. Java auch. Und Smalltalk. Und die Liste geht noch weiter. Jede dieser Sprachen verfügt über einen einzigartigen Funktionsumfang. Als wir uns entschieden hatten, OO zu LabVIEW hinzuzufügen, mussten wir uns zunächst Gedanken darüber machen, was das bedeutet. Waren die C++-Templates ein Merkmal von C++ oder ein Merkmal der OO? Und was ist mit Operator-Überladung? Oder das Schlüsselwort „synchronized“ (synchronisiert) von Java?

Wir haben beschlossen, dass OO für zwei Dinge steht: Kapselung und Vererbung. Diese beiden Säulen hatten wir im Hinterkopf, als wir uns überlegten, was wir unterstützen müssen, um uns als OO-Sprache zu bezeichnen. LabVIEW-Anwender sollten in der Lage sein, einen Klassendatentyp zu „kapseln“ – d. h., einen Datensatz wie z. B. einen Cluster zu definieren und LabVIEW anzuweisen, den Zugriff auf diese Daten nur in vom Anwender festgelegten Funktionen zuzulassen. Benutzer sollten auch in der Lage sein, eine neue Klasse zu „erben“ – eine bestehende Klasse auszuwählen und eine neue Klasse zu erstellen, die die bestehende Klasse als Startpunkt verwendet und Methoden der übergeordneten Klasse überschreibt. Der Fokus auf diese beiden Prinzipien half dabei, ein „Feature Creep“ zu verhindern.

Wie geht OO mit Datenfluss einher? (Die große Debatte um Wert- und Referenzparameter)
LabVIEW verwendet eine Datenflusssyntax. Ein Knoten in einem Diagramm arbeitet ausschließlich auf Basis seiner Eingänge, ohne die Werte anderer Verbindungen an anderer Stelle im Diagramm zu beeinflussen. Wenn sich eine Verbindung vergabelt, wird ihr Wert dupliziert. Diese Verbindungen verwenden eine wertbezogene Syntax („by value“), die dem Datenfluss entspricht. Referenztypen stellen dabei eine Ausnahme dar. Ein Referenztyp ist ein Referenzwert für einen gemeinsam genutzten Speicherplatz, der von mehreren Knoten durch einen Schutzmechanismus bearbeitet werden kann. Zu den Referenztypen in LabVIEW gehören die Queues, Datei-I/O, VI-Server-Typen usw. Wenn sich eine Verbindung verzweigt, wird der gemeinsame Speicher nicht dupliziert. Das Einzige, was vergabelt wird, ist die Zahl, mit der LabVIEW den gemeinsamen Speicher nachschlagen kann. Diese Drähte verwenden die referenzbezogene Syntax („by reference“). Bei der Entwicklung von LabVIEW-Klassen standen wir schon früh vor der Entscheidung, ob die Verbindungen der Klassen wert- oder referenzbezogen sein sollten.

Nehmen wir C++. In dieser Sprache gibt es zwei Möglichkeiten, ein Objekt zu deklarieren: auf dem Stack oder als Zeiger auf dem Heap. Wenn Sie das Objekt an eine Funktion übergeben oder es einer anderen Variablen zuweisen, müssen Sie sich stets darüber im Klaren sein, ob Sie das Objekt als Wert oder als Referenz übergeben. Haben Sie nur eine Kopie Ihrer Daten erstellt oder geben Sie Ihre Daten nun an jemand anderen weiter? Java und C# haben dagegen nur eine referenzbezogene Syntax. Variablenzuweisungen von Funktionsparametern referenzieren immer das ursprüngliche Objekt. Um ein Duplikat zu erstellen, erzeugen Sie explizit ein neues Objekt, indem Sie das Original als Quelle verwenden.

Welcher Ansatz wäre für LabVIEW geeignet? In einer Datenfluss-Sprache bedeutet die Verwendung einer wertbezogenen Syntax, dass der Wert auf jeder einzelnen Verbindung unabhängig von jeder anderen Verbindung bleibt. So können mehrere Threads im Programmcode ausgeführt werden, ohne dass die Gefahr besteht, dass ein Programmcodeabschnitt die Werte in einem anderen Abschnitt beeinflusst. Die referenzbezogene Syntax löst diese Unabhängigkeit auf. Plötzlich ist der Benutzer dafür verantwortlich, zu kontrollieren, wie oft ein Verweis freigegeben wurde, und sicherzustellen, dass kein Teil des Programmcodes zur gleichen Zeit wie ein anderer Teil auf den Verweis zugreift. Der Benutzer muss sich über geschützte Abschnitte, Mutexe und Laufzeitprobleme im Klaren sein. Der Vorteil ist, dass das Erstellen bestimmter Datenstrukturen, insbesondere zyklischer Graphen oder relationaler Datenbanken mit Referenzen einfacher ist. Diese oft komplexen Datenstrukturen sind ein wichtiger Grund für die Verwendung von OO.

LabVIEW verfügt bereits über Mechanismen zur gemeinsamen Datennutzung auf einer Verbindung. Obwohl diese Mechanismen einigen Standards nicht genügen, sind sie dennoch vorhanden und werden in jeder LabVIEW-Version verbessert. In LabVIEW wurde keine weitere Möglichkeit zur Datenfreigabe benötigt. Es sollte eine Möglichkeit geschaffen werden, Daten zu isolieren. Es ist schwer, Datenkonsistenz zu gewährleisten, wenn Änderungen an den Daten nicht begrenzt werden können. Um die Kapselung zu unterstützen, haben wir beschlossen, dass eine Klasse in LabVIEW im Grunde ein Cluster sein sollte, das nicht von allen VIs aufgeschlüsselt werden kann. Im Gegensatz zu Java, C# oder C++ verfügt LabVIEW also über eine rein wertbezogene Syntax für seine Objekte. Wenn sich eine Verbindung aufgabelt, kann das Objekt je nach Entscheidung des LabVIEW-Compilers dupliziert werden.

Wenn man statt referenzorientiert anhand des Wertes auswählt, hat das Auswirkungen auf alle anderen Entwurfsentscheidungen. Viele der standardmäßigen OO-Entwurfsmuster basieren darauf, dass ein Objekt auf ein anderes zeigen und sagen kann: „Ich kümmere mich um dieses Objekt dort drüben.“ LabVIEW stellt Mechanismen zur Erstellung von Datenreferenzen zur Verfügung – nicht initialisierte Schieberegister, globale Variablen, Queues mit einzelnen Elementen. Allerdings muss der Anwender mehr Arbeit investieren, um diese Mechanismen zu nutzen, als dies bei einer nativen referenzorientieren Syntax erforderlich wäre. Und doch haben sich diese anderen Mechanismen für das Erstellen vieler komplexer Datenstrukturen als ausreichend erwiesen. Es gibt jedoch ein Problem bei jeder referenzorientierten Lösung: Diese Strukturen sind nicht mit dem Datenfluss konsistent, sodass sie nicht von einer der größten Stärken von LabVIEW profitieren: der natürlichen parallelen Ausführung. Wenn Entwurfsmuster anderer Sprachen befolgt werden, führt dies zu Ineffizienzen und seltsamen Fehlern. Im Laufe der Zeit haben sich unsere eigenen Entwurfsmuster herauskristallisiert, die zeigen, wie leistungsfähig die wertorientierte Syntax sein kann.

Diese Entscheidung ist der größte Streitpunkt, wenn Leute zum ersten Mal mit LVOOP in Berührung kommen, aber das LabVIEW-Forschungsteam hat sich jahrelang damit beschäftigt und wir sind sehr zuversichtlich, dass dies die richtige Entscheidung für LabVIEW ist. Eine Verbesserung unserer referenzorientierten Fähigkeiten wird für die Zukunft in Betracht gezogen. Eine solide, wertorientierte Implementierung ist jedoch entscheidend für eine echte, datenflusskonforme Entwicklung.

[LV2009] In LabVIEW 2009 haben wir Datenwertreferenzen eingeführt. Dabei handelt es sich um einen neuen Referenztyp, der als Verweis auf beliebige LabVIEW-Daten dienen kann, sei es eine einfache Ganzzahl, ein Array, ein Cluster oder ein LabVIEW-Objekt. Auf diesen neuen Zusatz in der Sprache wird im weiteren Verlauf dieses Dokuments eingegangen.

Welche Aspekte wurden bei der Auswahl der Vokabeln in Betracht gezogen?
Der Name, der für ein Konzept ausgewählt wird, hat einen Einfluss darauf, wie die Menschen über dieses Konzept denken und wie einfach es für sie ist, das Konzept zu erlernen. Als wir die objektorientierte Programmierung in LabVIEW einführten, gab es eine Menge Diskussionen darüber, wie viel „Informatik“ man einfließen lassen sollte. Das Ziel von LabVIEW ist es, die Programmierung auch Anwendern ohne Informatikausbildung zugänglich zu machen. Zu unseren Kunden gehören Wissenschaftler und Ingenieure aus den Bereichen Prüf- und Messtechnik, industrielle Bedienelemente und Hardware-Design. Die Sprache von LabVIEW verstehen viele dieser Kunden in erster Linie aufgrund ihrer Ähnlichkeit mit einem Schaltplan. Wir haben versucht, für die verschiedenen OO-Konzepte Metaphern zu finden, die genauso greifbar sind.

Wir haben uns entschieden, für die Vererbungshierarchie Familienbegriffe zu verwenden: Eltern und Kind, Geschwister und Cousin. Diese Begriffe gehören bereits zum Wortschatz der Kunden. Wenn sich ein Referent auf das übergeordnete Objekt einer bestimmten Klasse bezieht, verstehen die Zuhörer die Beziehung und können dem Gespräch leicht folgen. Wir hätten auch Begriffe wie „Superklasse“ oder „Basisklasse“ verwenden können, aber dadurch wird nicht dieselbe unmittelbare Verbindung in den Köpfen der Menschen hergestellt wie durch die Familienbegriffe. Dem Lokalisierungsteam gefielen diese Begriffe wegen der einfachen Übersetzung. Wir haben außerdem vereinbart, dass in Benutzeroberflächen und Dokumentationen in den Vererbungsbäumen die Vorfahren immer oben und die Nachkommen unten stehen. In anderen Sprachen wurde schon allzu viel Zeit in Entwurfsmeetings von Entwicklern vergeudet, die nicht erkennen, dass die betrachteten Bäume auf dem Kopf stehen. Wir wollten eine einheitliche Vorgehensweise unter LabVIEW-Entwicklern fördern.

Bei den Begriffsbezeichnungen für den Umfang haben wir uns dafür entschieden, die Branchenstandards „privat“, „geschützt“ und „öffentlich“ zu übernehmen. Einige Entwickler hatten zunächst Einwände gegen das Wort „geschützt“. Privatdaten und öffentliche Daten sind intuitive Begrifflichkeiten. Das Wort „geschützt“ ist nicht besonders aussagekräftig – geschützt wovor? – und es lag nahe, ein Wort zu wählen, das den Anwendungsbereich tatsächlich identifiziert, wie z. B. „Familie“, was den Begrifflichkeiten entspricht, die wir für die Vererbung gewählt hatten. Der Begriff „geschützt“ wird jedoch in der gesamten Branche einheitlich verwendet, unabhängig von der Sprache, und die Benutzer würden diesen Begriff wahrscheinlich in jeder Hilfe oder jedem Lehrbuch eines Drittanbieters finden. Da der Begriff keinem bestehenden Konzept in LabVIEW widersprach, haben wir uns entschieden, bei den Standardbegriffen zu bleiben. [LV2009] Als wir den Bereich „Gemeinschaft“ einführten, gab es in den meisten anderen Programmiersprachen kein analoges Konzept. In diesem Fall haben wir unsere eigene Bezeichnung etabliert. Wenn der Vererbungsbaum „Familie“ war, dann bildeten Ihre Freunde Ihre „Gemeinschaft“. Wir waren der Meinung, dass dies am deutlichsten erklärt, wie „Gemeinschaft“ zu den anderen Bereichen passt. Wenn wir jedoch ehrlich sind, waren einige von uns enttäuscht, dass wir keinen Begriff finden konnten, der mit dem Buchstaben P beginnt, nur damit dieser zu den ursprünglichen drei Bereichen passt.

Das Wort „Klasse“ löste die längste Debatte aus. „Klasse“ ist offensichtlich der Branchenstandard für „einen Datensatz und die Funktionen, die auf diesen gruppierten Daten aufbauend arbeiten.“ Dabei handelt es sich um die Grundidee der OO-Programmierung. Aber wir haben länger darüber diskutiert, ob ein anderes Wort für unsere Benutzer greifbarer sein könnte. In LabVIEW gab es bereits die Idee eines Typedefs – ein Bedienelement-VI, das einen neuen Datentyp auf der Grundlage bestehender Typen definiert. In einem Vorschlag wurde das Schlüsselwort „Klasse“ vermieden und stattdessen von einer neuen Art von Typedef gesprochen: dem Objekt-Typedef. Obwohl dies für LabVIEW-Anwender, die OO zum ersten Mal sehen, konzeptionell greifbarer sein mag, waren die Vorreiter unter den Anwendern wahrscheinlich diejenigen, die OO bereits in anderen Sprachen gesehen hatten. Den Begriff „Klasse“ nicht zu verwenden, hätte diese Menschen verwirrt – wie kann eine Sprache ohne Klassen OO heißen? Also haben wir uns, wie bei „geschützt“, an den Branchenstandard gehalten. Wir haben in der Dokumentation darauf geachtet, zwischen „LabVIEW-Klassen“ und „VI-Serverklassen“ zu unterscheiden.

Die Konzepte „virtuell“ und „virtuelles Dispatching“ stellten für LabVIEW ein Problem dar. Auch diese Begriffe gelten als Branchenstandard. Virtuelle Funktionen sind in C#, C++ oder Java Funktionen, die basierend auf dem Laufzeitdatentyp eines der Eingänge (dem „this“-Objekt) verschiedene Versionen aufrufen. LabVIEW-Funktionen werden als „virtuelle Instrumente“ bezeichnet. Es wäre umständlich, wenn von „virtuellen virtuellen Instrumenten“ die Rede wäre. Außerdem schien „virtuell“ nie ein wirklich akkurater Begriff zu sein – es ist schwer zu verstehen, welcher Aspekt genau „virtuell“ an diesen Funktionen sein soll. Also haben wir uns für die Begriffe „dynamisch“ und „dynamische Bindung“ entschieden. Diese stehen im Kontrast zum Konzept der statischen Bindung, also die SubVI-Aufrufe von LabVIEW, die seit Jahrzehnten verwendet werden. Durch die Verwendung von „dynamisch“ und „statisch“ wird deutlich, was diese beiden Gruppen von VIs voneinander unterscheidet. Das Wort „statisch“ hat in anderen OO-Sprachen alle möglichen Bedeutungen, die für LabVIEW Probleme aufwerfen. Auf diesen Begriff wird im weiteren Verlauf dieses Dokuments eingegangen.

Es gab noch weitere kleinere Diskussionen über sonstige Begriffe. Die ausgewählten Bezeichnungen von OO sind ebenso ein Merkmal wie jeder tatsächliche Aspekt der Benutzeroberfläche. Um die OO verständlich zu gestalten, musste jedes neue Konzept in Frage gestellt werden. Wir konnten nicht davon ausgehen, dass der C++- oder Java-Name für eine Funktion der passende Name für LabVIEW ist.

Der Entwurf einer LabVIEW-Klasse


Wozu dient die Klasse „LabVIEW-Objekt“?
„LabVIEW-Objekt“ ist der Name einer bestimmten LabVIEW-Klasse. Diese spezielle Klasse ist der ultimative Vorfahre für alle LabVIEW-Klassen. JAVA besitzt eine ähnliche Klasse (java.lang.object). C++ tut dies nicht. C# verfügt über einen hybriden „Objekt“-Typ, der die Wurzel aller Klassentypen und eingebauten Typen wie Numerik ist.

Mit einer gemeinsamen Vorgängerklasse können Funktionen geschrieben werden, die beliebige Klassen verwenden und in einheitlicher Form mit diesen Klassen arbeiten. C++ kommt ohne eine solche Klasse aus, weil es Templates gibt, allerdings erhöht das Hinzufügen von Templates zu einer Sprache drastisch die Komplexität der Sprache. Obwohl LabVIEW in Zukunft über Klassentemplates verfügen könnte, waren wir der Meinung, dass eine weniger komplexe Lösung notwendig war. Eine gemeinsame Vorgängerklasse bietet einen Typ, der von Knoten wie Build Array verwendet werden kann, um Objekte aus mehreren Klassen in einem einzigen Array zu speichern. LabVIEW benötigt diese „generische Klasse“ wohl nicht, da es über den Datentyp „Variant“ verfügt, der beliebige LabVIEW-Daten enthalten kann. LabVIEW-Objekt kann sich jedoch mit der Funktion „nach spezifischer Klasse“ verbinden, wozu der Typ „Variant“ nicht in der Lage ist (und es wäre seltsam, wenn wir Änderungen vornehmen würden, sodass LabVIEW dies der Variante zulässt).

Ein spezieller Anwendungsfall für die Klasse LabVIEW-Objekt ist das dynamische Laden von Klassen in ein Framework. Es gibt zwei nennenswerte Funktionen, die ein LabVIEW-Objekt zurückgeben: die Eigenschaft „Standardinstanz“ einer LVClassLibrary-Referenz und die Funktion „Standartwert der LV-Klasse lesen.vi“. Beide Funktionen suchen eine Klasse (die eine über eine Referenznummer, die andere über einen Pfad) und geben eine Instanz der Klasse mit den Standardwerten für alle Felder zurück, wie sie im Privatdatenelement eingestellt sind. Mit dieser Funktion können Sie eine beliebige Klasse dynamisch laden und Klasseninstanzen erzeugen. Anwender greifen in „Plug-in“-Anwendungen auf diese gängige Technik zurück. Da für LabVIEW-Objekt keine Methoden definiert sind, sollten Sie die Funktion In spezifischere Klasse verwenden, um die Verbindung in einen Klassentyp zu konvertieren, für den Methoden definiert sind. Für eine Plug-in-Architektur würden Sie eine Klasse mit dem Namen „Generic Plugin.lvclass“ oder einem ähnlichen Namen definieren, in der alle Methoden definiert sind. Verwenden Sie diese Klasse als übergeordnete Klasse für alle ihre Plug-in-Objekte. Dann können Sie Unterklassen dynamisch laden. Die Referenztypen der Klasse werden in der Runtime-Engine von LabVIEW nicht unterstützt, sodass alles, was als DLL oder EXE erstellt oder auf ein Ziel heruntergeladen werden soll, das VI verwenden muss.

 

Original LV 8.2-Methode (funktioniert nicht in der Runtime-Engine).

  1. Offener Verweis auf die .lvclass-Datei auf der Festplatte. Dadurch wird eine LVClassLibrary-Referenznummer zurückgegeben.
  2. Ruft die Standardinstanz der Klasse ab.
  3. Casten Sie das LabVIEW-Objekt, das zurückgegeben wird, in Ihren Plug-in-Datentyp.

 

LV 8.5 und spätere Methode (funktioniert in der Runtime Engine)

  1. Laden Sie die Klasse mit der Datei „Standartwert der LV-Klasse laden.vi“ in den Speicher, die Sie in der Palette „Cluster, Klasse und Variante“ suchen.
  2. Wandeln Sie das LabVIEW-Objekt, das zurückgegeben wird, in Ihren Plug-in-Datentyp um.


Warum haben Klassen nur private Daten? Warum nicht geschützte oder öffentliche Daten?
So wie wir die in anderen Sprachen verwendeten Namen in Frage gestellt haben, haben wir auch ganze Funktionen hinterfragt. LabVIEW hat nur private Daten. Es können keine öffentlichen oder geschützten Daten erstellt werden. Nur VIs können öffentlich oder geschützt sein. 

Als triftigster Grund, alle Daten privat zu halten, gilt die Korrektheit des Programmcodes für die Benutzer. Wir haben versucht, die Sprache und die Umgebung von LabVIEW so zu gestalten, dass Anwender die bestmögliche Architektur auswählen können, auch wenn sie nicht darin geschult sind, diese auszuwählen. Das bedeutet, dass die Anzahl der vorausgesetzten Informatikkonzepte begrenzt ist. Der Zugriff auf die Daten einer Klasse allein über eine Methode sorgt für einen garantierten Engpass bei der Fehlerbehandlung von Wertänderungen. Es wird ein Platz geschaffen, an dem Programmcode für die Bereichsprüfung und andere Sicherheitsüberprüfungen hinzugefügt werden kann. Und dadurch wird sichergestellt, dass Programmcode außerhalb der Klasse keine binäre Abhängigkeit von der Klasse aufweist, sodass neue Revisionen der Klasse auch an erstellte Anwendungen weitergegeben werden können. Dies sind wesentliche Vorteile der Kapselung von Klassendaten, und wir möchten die Leute dazu ermutigen, Software zu entwickeln, die diese Vorteile von Natur aus mitbringt. Da wir keine öffentlichen oder geschützten Daten unterstützen, können wir ein potenziell verwirrendes Informatikkonzept aus der Sprache herausnehmen (zumindest streichen wir einen weiteren erklärungsbedürftigen Punkt) und die Leute zu einer besseren Architektur bewegen.

[LV2009] Daten können aus oben beschriebenen Gründen auch nicht in den Gemeinschaftsbereich aufgenommen werden.

Diese Entscheidung führt zu einer Vermehrung der Zugriffsmethoden. Das Erstellen von „Lese“- und „Schreib“-Methoden für jeden Datenwert in einer Klasse stellt sich bekanntermaßen als umständlich dar. Mit LabVIEW 8.5 wurde die Möglichkeit zur Erstellung von Zugriffs-VIs über ein Assistentendialogfeld eingeführt, und in LV2009 wurde dieses Dialogfeld um zusätzliche Optionen erweitert.

Wir waren besorgt über den Leistungs-Overhead eines SubVI-Aufrufs, anstatt die Daten einfach aufzuschlüsseln. Wir haben herausgefunden, dass unser aktueller Compiler-Overhead für den SubVI-Aufruf vernachlässigbar ist (gemessener Wert zwischen 6 und 10 Mikrosekunden auf einem durchschnittlichen PC). Der Vorgang des Aufschlüsselns bzw. Bündelns direkt am aufrufenden VI nimmt im Vergleich zum Aufschlüsseln in einem SubVI fast die gleiche Zeit in Anspruch. Wir sehen ein Problem mit Datenkopien für aufgeschlüsselte Elemente, da ein Großteil des Inplaceness-Algorithmus von LabVIEW nicht über SubVI-Grenzen hinweg arbeitet. Als Umgehungslösung können Sie auch Zugriffsmethoden zugunsten von Methoden auswählen, die tatsächlich die Arbeit im SubVI erledigen, um zu vermeiden, dass Elemente aus dem SubVI zurückgegeben werden, sodass Datenkopien vermieden werden. Diese Option ist in vielen Fällen ohnehin das bessere OO-Design, da sie verhindert, dass die private Implementierung einer Klasse als Teil ihrer öffentlichen API offengelegt wird. Wir erwarten, dass in zukünftigen Versionen das Inlining von SubVIs möglich sein wird, was den Overhead vollständig beseitigen würde.  

Wir sind der Meinung, dass es langfristig besser ist, die Editor-Erfahrung und die Compiler-Effizienz für diese VIs zu verbessern, als öffentliche/geschützte/gemeinschaftliche Daten einzuführen. 

Wo sind die Konstruktoren?
Diese Frage wird in der Regel frustriert von jemandem gestellt, der OO aus einer anderen Programmiersprache kennt und seit ein paar Stunden mit LVOOP arbeitet. Etwas, das so grundlegend für die Sprache ist, kann nicht so vollständig versteckt werden, und die Person schämt sich entweder dafür, dass sie es nicht finden kann, oder ist wütend auf uns, weil wir es so schwer auffindbar gestaltet haben! Aber die Antwort ist einfach: Es sind keine Konstruktoren vorhanden.

Werfen wir einen Blick auf die Anwendungsfälle für Konstruktoren:

  1. Einstellung der Anfangswerte eines Objekts.
  2. Berechnung der Anfangswerte des Objekts aus einem Parametersatz.
  3. Berechnung der Anfangswerte des Objekts aus Umgebungsdaten (z. B. Aufzeichnung des Zeitstempels, zu dem das Objekt erstellt wurde).
  4. Reservierung von Systemressourcen (Speicher, Kommunikationskanäle, Hardware usw.) für die Verwendung durch dieses Objekt (um später durch den Destruktor wieder freigegeben zu werden).

In LabVIEW besteht die Möglichkeit, den Standardwert für Ihre Klasse festzulegen. Die Werte, die Sie in Ihrem Privatdatenelement eingestellt haben, sind die Standardwerte für Ihre Klasse. Handelt es sich nun um berechnete Werte? Nein, sie sind statisch. Es steht kein Bereich zur Verfügung, um ausgeführten Programmcode als Teil des Standardwerts zu speichern. Dies ist vergleichbar mit einem einfachen Standardkonstruktor in anderen OO-Sprachen. Der Anfangswert für alle Instanzen der Klasse ist genau dieser Standardwert, so dass der erste Konstruktoranwendungsfall Nr. 1 erfüllt ist.

Wie deklariert man in C++ einen Integer?

int k = 1;

Wie deklariert man in LabVIEW einen Integer?

Sie legen ein numerisches Bedienelement ab, stellen seine Darstellung auf Integer ein, geben eine 1 ein und legen den aktuellen Wert als Standard fest.


Schauen Sie sich diese C++-Klasse an...

class Thing {
private:
int mx;
double mz;
std::string ms;
public:
Thing() : mx(1), mz(2.5), ms("abc") { }
Thing(int x) : mx(x), ms("def") {
mz = (x > 1) ? 3.5 : 2.5;
}
~Thing() { }
void Init(int x) {
mx = x; mz = (x > 1) ? 3.5 : 2.5; ms = "def";
}
};


Wie ruft man in C++ einen nicht standardmäßigen Konstruktor auf?

Thing item(k+3);

Wie ruft man in LabVIEW einen nicht standardmäßigen Konstruktor auf?

Das geht nicht. Dies ist wirklich keine aussagekräftige Frage für LabVIEW. Sie übergeben keine Parameter an Bedienelemente, um deren Werte zu initialisieren. Sie beziehen ihren Wert entweder von einem Parameter, der an das VI übergeben wird, oder von einem Wert, der vom Benutzer auf dem Frontpanel eingegeben wird.


Das Konzept eines Konstruktors stellt in der Datenflusswelt ein Problem dar. Alles beginnt entweder mit einem Wert (komplett ohne externe Eingaben berechnet) oder es gibt einfließende Werte und es wird berechnet, wann diese Werte eintreffen. Eine LabVIEW-Klasse verfügt bei der Erstellung über einen Anfangswert, der durch den Datentyp im Privatdatenelement festgelegt wird. Das ist der erste Konstruktor-Anwendungsfall. Alle Instanzen werden auf denselben Wert initialisiert. Wenn weitere Funktionen gewünscht sind, sollten diese im Blockdiagramm definiert werden, indem ein SubVI erstellt wird, das Eingänge von Nicht-Klassen aufnimmt und eine Klasse ausgibt. Das ist der zweite Konstruktor-Anwendungsfall.

Der dritte Konstruktoranwendungsfall ist ein fortgeschritteneres Konzept für Softwareentwickler. LabVIEW bietet Unterstützung für dieses Konzept, allerdings ist dies nicht offensichtlich. In den meisten Klassen wird keine Funktion benötigt, um zu zählen, wie viele Instanzen der Klasse existieren (mit Hilfe eines statischen Feldes der Klasse), oder um den Zeitstempel einer bestimmten Instanzierung aufzuzeichnen. Wir haben diese Funktionen für ein erweitertes Werkzeug vorgesehen. Sie konstruieren ein XControl von Typ Ihrer Klasse. Das XControl verfügt über Programmcode, der zur Bearbeitungszeit und zur Laufzeit ausgeführt wird, um Werte der Klasse zu initialisieren. In das Fassaden-VI können Sie beliebigen Programmcode einfügen, um die Werte für die Instanz Ihrer Daten festzulegen. Für die Ersteller der Klasse erscheint dies heute vielleicht etwas umständlich. Das ist sicherlich etwas, das wir mit der Zeit einfacher gestalten wollen. Aber es ist der geeignete Ort, um diese Funktionen unterzubringen – im Programmcode, der dem Bedienelement zugrunde liegt.

Der vierte Konstuktoranwendungsfall kann nur in Verbindung mit der nächsten Frage diskutiert werden.

Wo sind die Destruktoren?
Wie lange ist die Lebensdauer der Integer in C++?

Er bleibt vom Zeitpunkt der Deklaration bis zur schließenden Klammer bestehen.

Wie lange ist die Lebensdauer der Integer in LabVIEW?

Nicht bekannt. Bis zum Ende der Verbindung? Bis das Frontpanel schließt? Bis das VI nicht mehr ausgeführt wird? Im Wesentlichen handelt es sich nur um einen Wert auf einer Verbindung oder in einem Bedienelement/Anzeigeelement. Sie bleibt so lange bestehen, bis das Objekt nicht mehr benötigt wird.


LabVIEW verfügt nicht über „räumliche Lebensdauer“. LabVIEW besitzt eine „zeitliche Lebensdauer“. Ein Datensatz bleibt so lange bestehen, wie er benötigt wird, und geht verloren, wenn er nicht mehr benötigt wird. Wenn es in das Frontpanel-Bedienelement kopiert wird, bleibt es dort, auch nachdem die Ausführung beendet ist. Kopien auf den Verbindungen bleiben bis zur nächsten Ausführung dieser Verbindung bestehen. Eine weitere Kopie wird für jede Sonde auf der Verbindung erstellt. In einer rein theoretischen Datenflusssprache gäbe es auf jeder einzelnen Verbindung eine eigene Kopie, da jede Verbindung eine eigenständige Recheneinheit darstellt. Das wäre natürlich ineffizient zu implementieren, daher optimiert der LabVIEW-Compiler die Anzahl der Kopien. Das Prinzip ist jedoch dasselbe: Daten bleiben lange bestehen und überleben manchmal das Programm, das diese Daten erzeugt hat.

Ziehen Sie in einer solchen Umgebung den vierten Konstruktoranwendungsfall in Betracht. Wann würde der Konstruktor Ressourcen reservieren? Was würde jedes Mal passieren, wenn eine Kopie erstellt wurde? Würden Sie unterschiedliche Kopierkonstruktoren für das T-Stück an einer Verbindung und für das T-Stück in ein Anzeigeelement und für das T-Stück in eine Sonde benötigen? Wenn Sie versuchen, Antworten auf diese Fragen zu finden, werden Sie viele Sonderfälle und Ausnahmen auflisten. Die Entscheidung, wann genau diese impliziten Konstruktoren ausgeführt werden sollen, um Ressourcen zu reservieren, ist sehr schwierig. Wenn Sie zudem zufriedenstellende Antworten gesucht haben, müssen Sie immer noch entscheiden, wann der Destruktor ausgeführt werden soll, um diese Ressourcen freizugeben.

Die Ressourcenreservierung in LabVIEW wird über Referenztypen verarbeitet. Die Referenztypen kopieren die Referenznummer wertbezogen auf die Verbindung. Zudem müssen Sie explizit eine Funktion aufrufen, um eine „tiefe Kopie“ zu erstellen oder sie freizugeben. Der Queue-Datentyp verfügt z. B. über eine die Funktion „Queue anfordern“, mit der die Referenz reserviert wird, und eine explizite Funktion „Queue freigeben“, mit der sie wieder freigegeben wird. Sofern Sie nicht „Queue freigeben“ aufrufen, befindet sich die Queue so lange im Speicher, bis das VI seine Ausführung beendet hat. Zu diesem Zeitpunkt wird sie von LabVIEW implizit freigegeben.

Ohne ein verbessertes Konzept der „Lebensdauer“ ist der vierte Anwendungsfall nicht realisierbar.

Zusammenfassend lässt sich sagen, dass LabVIEW als Datenflusssprache normalerweise keine Variablen besitzt. Verbindungen sind keine Variablen. Bedienelemente auf dem Frontpanel sind keine Variablen. Auch lokalen oder globalen Variablen wird keine spezifische Lebensdauer zugewiesen, wie dies bei Variablen in anderen Sprachen der Fall ist. Variablen sind Teil eines Modells, das im Widerspruch zum Datenfluss steht. Ohne Variablen und eine Möglichkeit, die Lebensdauer dieser Variablen zu definieren, sind Konstruktion und Destruktion bedeutungslose Konzepte, weshalb wir sie im ursprünglichen Sprachdesign weggelassen haben.

[LV2009] Die Datenwertreferenzen bieten eine Lösung für dieses Problem. Da es sich um Referenzdatentypen handelt, haben sie eine genau definierte Lebensdauer. DWRs können für jeden LabVIEW-Datentyp erstellt werden, aber wir haben für LabVIEW-Klassen etwas Besonderes gemacht. Im Dialogfeld „Klasseneigenschaften“ gibt es eine standardmäßig aktivierte Option für eine Klasse, um das Erstellen und Zerstören von DWRs von Objekten dieser Klasse nur auf Mitglieds-VIs der Klasse zu beschränken. Anders ausgedrückt, nur die Bestandteil-VIs der Klasse können Referenzen auf die Klasse erstellen. Jedes andere VI, das versucht, eine Verbindung des Klassentyps mit dem Grundobjekt „Neue Datenwertreferenz“ herzustellen, ist nicht ausführbar. Diese Einschränkung hat zur Folge, dass eine Klasse Programmcode in ihre Bestandteil-VIs einfügen kann, um zu gewährleisten, dass niemals ein Verweis auf die Klasse erstellt wird, ohne dass ihr ein korrekter Wert (wie vom Urheber der Klasse definiert) zugewiesen wird. Ebenso kann eine Referenz niemals zerstört werden, außer durch eine Bestandteil-VI, die dem Urheber der Klasse einen Freigabecode zur Verfügung stellt. Dieser Freigabecode wird nicht aufgerufen, wenn der Verweis implizit durch ein anhaltendes VI freigegeben wird. Solange Sie jedoch jeden erstellten Verweis immer explizit aufheben, bieten die DWRs einen lebenslangen Geltungsbereich für LabVIEW-Objekte und damit Unterstützung für den vierten Anwendungsfall.

Es sollte beachtet werden: Referenztypen sollten in Ihren VIs äußerst selten verwendet werden. Auf der Grundlage von zahlreichen Kunden-VIs gehen wir in der F&E davon aus, dass von allen LabVIEW-Klassen, die Sie jemals erstellen, weniger als 5 % auf Referenzmechanismen angewiesen sind. Wenn Ihre Anforderungen darüber hinausgehen, sollten Sie darüber nachdenken, wie Sie mehr wertorientierte Klassen verwenden können, da Sie mit ziemlicher Sicherheit unnötig Leistung einbüßen.

Was versteht man unter dem speicherinterne Layout einer Klasse?
Die effiziente Speicherung von Klassen in LabVIEW war eine interessante Herausforderung.

Die Daten einer Klasse bestehen aus zwei Teilen:

  1. Der von seinem übergeordneten Objekt vererbte Datenblock.
  2. Ein eigener privater Daten-Cluster


Sie können sich eine beliebige Klasse als einen Cluster von Clustern vorstellen – wobei jeder der inneren Cluster von einem seiner Vorfahren stammt, mit Ausnahme des letzten, bei dem es sich um den privaten Daten-Cluster der Klasse selbst handelt. Der ultimative Vorfahre, LabVIEW-Objekt, steuert keine Datenfelder bei.


LabVIEW verwendet keinen Funktionsstack. Der Datenfluss, bei dem sich Knoten auf verschiedenen, parallel ausgeführten Diagrammen miteinander verschachteln können, erschwert die Anwendung einer Stack-Architektur. Stattdessen weist LabVIEW beim Kompilieren eines VIs einen „Datenbereich“ für dieses VI zu. Der Datenbereich ist eine Zuordnung aller Daten, die zur Ausführung dieses VIs benötigt werden. Jeder Thread weiß, dass er in seinen Bereich des Datenraums schreiben kann, ohne sich Sorgen machen zu müssen, dass ein Schreibvorgang eines anderen Threads stattfindet, so dass kein Mutex-Sperren erforderlich ist.

Um Klassen zu implementieren, mussten wir in der Lage sein, eine Klasse im Datenraum zuzuweisen. Eine Verbindung, die vom übergeordneten Typ stammt, muss in der Lage sein, Daten ihres eigenen Typs oder eines beliebigen nachgeordneten Typs zu übertragen, daher muss die Zuweisung, die wir im Datenraum vornehmen, in der Lage sein, eine der Klassen aufzunehmen. Das bedeutet, dass wir diese Cluster nicht einfach direkt von Clustern zuordnen können.

Um das Design noch komplizierter zu gestalten, muss ein Objekt seine Klasseninformationen mit sich herumführen. Ein Objekt ist ein sich seiner selbst bewusster Teil von Daten. Es hat Kenntnis von seinem eigenen Typ (deshalb können Objekte Operationen wie „Nach spezifischer Klasse“ und „dynamische Bindung“ durchführen). Die Klasseninformation muss irgendwann einmal Teil des Objekts sein.

Die endgültige Speicherstruktur sieht also wie folgt aus:



LabVIEW weist eine einzelne Instanz des Standardwerts der Klasse zu. Jedes Objekt, das den Standardwert darstellt, teilt diesen Zeiger. Diese gemeinsame Nutzung führt dazu, dass bei einem sehr großen Array als Standardwert Ihrer Klasse zunächst nur eine Kopie dieses Arrays im Speicher vorhanden ist. Wenn Sie in die Daten eines Objekts schreiben, dupliziert LabVIEW den Standarddatenzeiger, wenn dieses Objekt derzeit den Standardwert teilt, und führt dann den Schreibvorgang für das Duplikat durch. Durch die gemeinsame Nutzung der Standardinstanz verbraucht das gesamte System viel weniger Speicher und das Öffnen eines VIs geht wesentlich schneller vonstatten.

Kann ich Klassen rekursiv definieren?
Die Daten im Privatdatenelement können ein beliebiger wohldefinierter LabVIEW-Typ sein, einschließlich anderer LabVIEW-Klassen. Für Klassen, die nur Methoden definieren und keine eigenen Daten besitzen, kann der Cluster sogar leer gelassen werden.

Eine rekursive Klasse ist eine Klasse, die ihre eigene Definition als Teil von sich selbst verwendet. Diese Klassenart wird auch in anderen Objektsprachen angezeigt. In C++ würden Sie zum Beispiel Folgendes sehen:

class XYZ {
int data;
XYZ *anotherInstanceOfThisType;
};

Es scheint also, dass die Klasse ihren eigenen Typ verwendet, um sich selbst zu definieren. Dies ist nicht ganz richtig. Technisch gesehen, ist der Feldtyp eigentlich „Zeiger auf XYZ“, nicht „XYZ“ selbst. Diese technische Unterscheidung ist wichtig, um zu verstehen, wie diese anderen Sprachen Speicher für ein rekursiv definiertes Objekt zuweisen können. Sie reservieren nicht unmittelbar den Speicherplatz für das Objekt. Sie reservieren Speicherplatz für einen Verweis auf ein solches Objekt.

Wenn Sie ein Bedienelement einer LabVIEW-Klasse in den privaten Daten-Cluster aufnehmen, wird die gesamte Klasse in die Privatdaten aufgenommen. Es ist keine Referenz auf die Klasse. Daher kann eine Klasse nicht in ihren eigenen Privatdaten enthalten sein.

[LV2009] LabVIEW verfügt über Referenzelemente. Die Referenzelemente „Queue“ und „Neuer Datenwert“ können verwendet werden, um den Privatdaten einer Klasse einen Verweis auf eine andere Klasse zu vergeben. Viele Anwender erwarten vielleicht, dass mit dieser Technik ein Referenzelement in die Klasse X eingefügt werden kann. Technisch wäre das möglich, aber LabVIEW unterbindet dies. Dieses Thema wurde in der F&E sehr kontrovers diskutiert. Der Versuch zu erklären, welche anderen Klassen in einer bestimmten Klasse legal verwendet werden können und unter welchen Bedingungen, könnte für Personen verwirrend sein, die mit der Programmierung im Allgemeinen oder der OO im Speziellen weniger vertraut sind. Schließlich haben wir uns auf eine feste und schnelle Regel geeinigt: Die Klasse X kann jede Klasse enthalten, die nicht selbst die Klasse X verwendet. Ganz einfach. Eine übergeordnete Klasse kann ihre eigene Unterklasse nicht einschließen, da die Unterklasse die übergeordnete Klasse verwendet. Eine Unterklasse kann jedoch die übergeordnete Klasse benutzen, weil die übergeordnete Klasse die Unterklasse nicht benutzt. Diese Regel gewährleistet eine sehr saubere Funktionsweise beim Laden und Entladen, ohne dass Sie sich Gedanken über zirkuläre Abhängigkeiten machen müssen.

Wenn die Klasse X einen Verweis auf die Klasse X enthalten soll, verwenden Sie stattdessen einen Verweis auf die übergeordnete Klasse von X. Da ein Unterobjekt immer als übergeordneter Wert gültig ist (in einem übergeordneten Bedienelement oder auf einer Verbindung vom übergeordneten Typ), können Sie Ihre Referenzen auf diese Weise erstellen.

In LabVIEW OOP gibt es eine Besonderheit, die für Anwender anderer Programmiersprachen unlogisch erscheinen mag. Eine Unterklasse kann die übergeordnete Klasse als Datenelement der Unterklasse einschließen. Die übergeordnete Klasse ist ohne die Unterklasse vollständig definiert und kann daher als Teil des privaten Daten-Clusters der Unterklasse verwendet werden, ohne eine rekursive Typdefinition zu erstellen. Während der Ausführung können Sie jeden beliebigen Wert an dieser Position speichern – einschließlich einer anderen Instanz der Unterklasse selbst, ohne dass dafür überhaupt ein Referenzdatentyp erforderlich wäre. Sie können also eine Kette von Objekten desselben Typs erstellen, die durch eine Instanz der übergeordneten Klasse abgeschlossen wird. Tatsächlich kann jede verknüpfte Listen- oder Baumdatenstruktur auf diese Weise erstellt werden – und diese Datenstrukturen sind vollständig datenflusssicher. Beispiele dafür wurden an verschiedenen Stellen auf ni.com und lavag.org veröffentlicht.

Wie erzeuge ich Klassendaten (auch als statische Daten bekannt)?
Klassendaten sind Daten, die nicht Teil der Objektinstanzen sind. Es wird im Speicher nur eine Kopie der Klassendaten angelegt und der Zugriff auf diese einzelne Instanz kann öffentlich, geschützt oder privat definiert sein. Es gibt mehrere Möglichkeiten, Klassendaten zu erstellen.

Am einfachsten geht das mit dem globalen VI. Durch das Hinzufügen des globalen VIs in Ihre Klasse und die anschließende Einstellung des Bereichs als privat oder geschützt wird eingeschränkt, welche anderen VIs auf diese Daten zugreifen können. Allerdings sind globale VIs, selbst wenn sie innerhalb von Klassen skaliert sind, grundsätzlich nicht zulässig – sie sind nicht thread-sicher, da sie außerhalb des Datenflusses operieren. Zudem haben sie einen erheblichen Leistungs-Overhead (sogar für einen reinen Lesevorgang ist eine Kopie erforderlich).

Besser geeignet ist ein VI mit einem nicht initialisierten Schieberegister, das in der Online-Hilfe als „LV2-style global“ bezeichnet wird. Diese Methode zur Erstellung globaler Daten gibt es in LV seit LabVIEW 2.0 (daher der Name). Sie können dieses VI zu einem Mitglied einer Klasse machen und dann den Bereich auf privat oder geschützt einstellen. Mit solchen globalen Variablen können Sie die Menge der für Ihre Daten verfügbaren thread-sicheren Operationen festlegen. Weitere Informationen finden Sie in der LabVIEW-Hilfe oder in den Beispiel-VIs. Wir stellen ein konkretes Beispiel zur Verfügung, in dem der Branchenstandard „Singleton“ als Entwurfsmuster dargestellt wird.

Der Entwurf von Klassenmethoden


Welche Verbindung ist das „this“-Objekt? Wie erzeuge ich klassenstatische Methoden?
C++ und Java verwenden beide das Konzept eines „this“-Objekts. Wenn Sie in diesen Sprachen die Methode eines Objekts definieren, geben Sie die expliziten Parameter für die Funktion an. Die Sprache geht von einem zusätzlichen impliziten Parameter aus, dem Objekt, auf dem die Methode operiert. Der implizite Parameter wird als „this“-Objekt bezeichnet. Für den Zugriff auf Teile dieses Objekts wird eine spezielle Syntax verwendet.

In diesen Sprachen bezeichnet eine „klassenstatische Methode“ eine Funktion, die Teil der Klasse ist, aber keinen impliziten Parameter hat. Die spezielle „this“-Syntax kann in einer statischen Methode einer Klasse nicht verwendet werden, da es kein impliziertes Objekt gibt.

In LabVIEW ist keines dieser Konzepte enthalten. Sie deklarieren alle Eingänge zu Ihren Bestandteil-VIs explizit im Anschlussfeld. LabVIEW fügt niemals einen impliziten Eingang hinzu. Da kein impliziter Eingang vorhanden ist, besteht kein Unterschied zwischen einem VI mit einem solchen Eingang und einem VI ohne Eingang.

LabVIEW umfasst zwar Methoden, die wir als „statisch“ bezeichnen, aber wir verwenden den Begriff in einem anderen Zusammenhang als die übrigen Sprachen. LabVIEW unterscheidet zwischen zwei Arten von Methoden: dynamische Methoden und statische Methoden. Eine statische Methode ist ein einfacher SubVI-Aufruf, über den LabVIEW seit seiner Gründung verfügt. Eine Methode wird als „statisch“ bezeichnet, weil der SubVI-Knoten immer das gleiche SubVI aufruft. Im Gegensatz dazu besteht eine dynamische Methode aus einer Reihe von VIs. Ein dynamischer SubVI-Knoten nutzt die dynamische Bindung, um eines der VIs im Satz aufzurufen, aber welches genau, ist erst zum Zeitpunkt der Ausführung bekannt.

Wie funktioniert die dynamische Bindung? Wie sieht der Overhead im Vergleich zu einem normalen SubVI-Aufruf aus?
Die Dynamische Bindung ist die Komponente, bei der ein Knoten, der wie ein SubVI-Aufruf aussieht, in Wahrheit eines von mehreren SubVIs zur Laufzeit aufruft, abhängig von dem Wert auf der Verbindung am Eingangsanschluss für dynamische Bindung. Konzeptionell entspricht dieses VI einem polymorphen VI, das auswählt, welches VI während der Ausführung und nicht während der Kompilierung ausgeführt werden soll.

Aufgrund des VI-Servers, der dynamische Aufrufe beliebiger VIs unterstützt, gehen Tester oft davon aus, dass die dynamische Bindung im Vergleich zu SubVI-Aufrufen langsam abläuft. Der Compiler besitzt jedoch viel mehr Informationen über den spezifischen Satz von VIs, die bei einem Aufruf mit dynamischer Bindung aufgerufen werden können, als bei einem generischen VI-Serveraufruf, wodurch eine bessere Leistung erzielt werden kann.

Jedes Objekt, das auf der Verbindung transportiert wird, führt einen Zeiger auf seine Klasseninformationen mit sich (siehe Abschnitt „Was versteht man unter dem In-Memory-Layout einer Klasse?“ weiter oben in diesem Dokument). Diese Klasseninformationen enthalten die „Tabelle der dynamischen Bindungen“, die eine Tabelle mit VI-Referenzen darstellt. Jede Klasse kopiert die Tabelle ihrer übergeordneten Klasse genau. Es ersetzt dann die VI-Referenz für jede übergeordnete Funktion durch seine eigenen überschreibenden VIs. Die Klasse hängt ihre gesamten dynamischen Dispatch-VIs, die keine Überschreibungen ihrer übergeordneten Klasse darstellen, an ihre Tabelle an.

  • Parent.lvclass definiert zwei dynamische Dispatch-VIs: A.vi und B.vi.
  • Child.lvclass erbt von der übergeordneten Klasse. Es definiert zwei dynamische Dispatch-VIs: B.vi und C.vi. B.vi überschreibt den zweiten Eintrag in der Tabelle.
  • Junior.lvclass erbt vom Unterobjekt. Es definiert zwei dynamische Dispatch-VIs: A.vi und D.vi. A.vi überschreibt den ersten Tabelleneintrag.

Im Diagramm zeichnet ein SubVI-Knoten mit dynamischer Bindung eine bestimmte Indexnummer auf, wenn er kompiliert wird. Ein Knoten, der z. B. einen Aufruf von A.vi darstellt, erfasst den Index 0. Ein Knoten für einen Aufruf von B.vi würde den Index 1 erfassen. Während der Ausführung, wenn ein Objekt über die Verbindung kommt, greift der Knoten auf die Tabelle der dynamischen Bindung dieses Objekts zu. Es ruft das VI mit dem aufgezeichneten Index ab und startet das VI. Für den Knoten fällt kein Overhead für die Namenssuche oder das Durchsuchen von Listen an, um herauszufinden, welches SubVI aufgerufen werden soll. Die Zeitkomplexität ist O(1), unabhängig davon, wie tief der Vererbungsbaum geht oder wie viele dynamische Dispatch-VIs die Klassen definieren.

Von dort aus erfolgt meist nur ein SubVI-Aufruf. Es kann zu Leistungseinbußen kommen, da LV die Inplaceness (Speicherduplizierung) bei einem dynamischen Bindungsaufruf nicht so gut optimieren kann wie beim Aufruf einer statischen Bindung. LabVIEW vermeidet dies, indem es für die Inplaceness des ältesten Vorgängers eine Optimierung vornimmt – ein Aufruf von B.vi würde also die Parent.lvclass-Version von B.vi verwenden, um zu entscheiden, ob Kopien der Eingänge benötigt werden oder nicht. In den meisten Fällen suchen wir, da die untergeordneten Überschreiben-VIs dasselbe Anschlussfeld verwenden und dieselbe Funktion wie das übergeordnete VI erfüllen, tendenziell dieselben Implaceness-Muster. Wenn die Inplaceness-Muster übereinstimmen, ist der Overhead derselbe wie bei einem SubVI-Aufruf. Dies bedeutet, dass Sie möglicherweise einen Leistungsvorteil erhalten, wenn Sie Eingänge mit Ausgängen von Vorfahren-Implementierungen verbinden, selbst wenn Sie nicht erwarten, dass die Vorfahren-Implementierungen jemals direkt aufgerufen werden (z. B. wenn Sie den Vorfahren als abstrakte Klasse verwenden).

Kann ich VI-Namen in Klassen überladen?
Nein. Das Überladen wird in anderen OO-Sprachen verwendet, wenn zwei Funktionen exakt den gleichen Namen, aber eine unterschiedliche Parameterliste haben. Der Compiler findet anhand der im Funktionsaufruf angegebenen Parameter heraus, welche Funktion der Programmierer aufrufen wollte. Mit diesem Funktionsprinzip können Sie z. B. mehrere Funktionen mit dem Namen „Init“ haben, eine, die die Initialisierung des Objekts aus einer Datei veranlasst (und somit einen Pfad als einzigen Parameter entgegennimmt) und eine andere, die eine Initialisierung aus einem anderen Objekt veranlasst (und somit dieses Objekt als Parameter entgegennimmt).

Diese Funktion ist die Ursache einiger schwerwiegender Fehler in diesen anderen Sprachen, insbesondere wenn es sich um dynamische Bindungen handelt. Wenn jemand die Parameterliste in der übergeordneten Klasse ändert, aber vergisst, die Parameterliste in der Unterklasse zu ändern, betrachtet der Compiler dies als zwei verschiedene Funktionsdeklarationen und nicht als Compilerfehler. Alle Fehler treten während der Ausführung auf, und was genau schiefläuft, lässt sich nur sehr schwer herausfinden. In LabVIEW ist es unter keinen Umständen erlaubt, dass sich zwei VIs mit demselben Namen im Speicher befinden. Das Hinzufügen von Überladungen, einer Funktion, die eine neue Klasse von schwer zu behebenden Fehlern erzeugt, war kein triftiger Grund für uns, diese Regel zu ändern.

Erweiterte OO-Komponenten-Unterstützung


Gibt es eine Möglichkeit, eine Klasse zu einem Freundobjekt einer anderen Klasse zu deklarieren?
[LV2009] Ja, in LabVIEW 2009 und neueren Versionen. Jede Bibliothek (.lvlib, .lvclass, .xctl, .lvsc) kann eine Liste von Freund-VIs oder Freund-Bibliotheken deklarieren. Indem die Bibliothek ein VI zum Freund erklärt, erteilt sie diesem VI eine Sondererlaubnis (wir werden gleich darauf eingehen, was diese Erlaubnis ist). Indem eine andere Bibliothek als Freund deklariert wird, erteilt diese Bibliothek allen Bestandteil-VIs dieser anderen Bibliothek die Sondergenehmigung. 

Die Sondergenehmigung ist das Recht zum Aufruf von Bestandteil-VIs aus dem Gemeinschaftsbereich. Angenommen, eine Bibliothek X besitzt mehrere Bestandteil-VIs, A.vi, B.vi und C.vi. Nun wird A.vi als privater Bereich markiert. Das bedeutet, dass nur andere VIs, die Bestandteil von X sind, A.vi aufrufen können. B.vi ist öffentlich zugänglich, sodass es von jedem anderen VI aufgerufen werden kann. Aber C.vi gehört zum Gemeinschaftsbereich. Das bedeutet, dass es nur von Mitgliedern der Bibliothek X und von den befreundeten VIs außerhalb der Bibliothek X aufgerufen werden kann.

Für alle, die mit diesem Konzept nicht vertraut sind: Die Deklaration einer anderen Klasse als „Freund“ dieser Klasse gibt dieser anderen Klasse die Erlaubnis, auf die privaten Bereiche dieser Klasse zuzugreifen. Die „Freund von“-Syntax ist für einige APIs wichtig. Ein klassisches Beispiel ist eine Matrixklasse gemeinsam mit einer Vektorklasse – die Funktion, die eine Matrix mal einen Vektor multipliziert, benötigt Zugriff auf die privaten internen Bereiche der beiden Klassen.

Warum haben wir einen neuen Bereich „Gemeinschaft“ erstellt?
[LV2009] In anderen Programmiersprachen, in denen das „Freund“-Konzept zum Einsatz kommt, erhalten Freunde in der Regel weitreichenden Zugriff auf alle privaten Bereiche. Wenn Sie einer Freundklasse Zugriff auf Ihre gesamten privaten Methoden und Daten gewähren, öffnen sich zu viele Hintertüren, durch die Daten geändert werden können, ohne dass dies über die definierten VIs erfolgt. Selbst wenn sie nur Zugriff auf die privaten Methoden erhalten, ist das fast immer zu aufwändig. Meistens benötigen die Freunde nur Zugriff auf eine bestimmte Methode. Durch den Gemeinschaftsbereich können die privaten Teile der Bibliothek immer noch nach Belieben geändert werden, ohne externe Abhängigkeiten zu verletzen. Außerdem lässt sich in der Liste der auf die Gemeinschaft beschränkten VIs besser nachvollziehen, warum ein bestimmtes VI oder eine Bibliothek als Freund benannt wurde. In Programmiersprachen kommt es allzu oft vor, dass ein Element mit einem anderen befreundet ist und sich niemand mehr daran erinnert, warum diese Freundschaft notwendig war. 

Warum können Unterklassen nicht auf VIs mit Gemeinschaftsbereich zugreifen?
[LV2009] Die VIs, die sich im Gemeinschaftsbereich befinden, können von der Eigentümerbibliothek und den Freunden dieser Bibliothek aufgerufen werden. Wenn es sich bei der Bibliothek um eine Klasse handelt (im Gegensatz zu einer Projektbibliothek, XControl oder StateChart), dann kann die Klasse Unterklassen haben. Die Bestandteil-VIs von Unterklassen können keine VIs mit Gemeinschaftsbereich aufrufen. Manchmal möchten Sie, dass dieselbe Funktionalität von Ihren Freunden und von Ihrer Familie genutzt werden kann. Diese Aufteilung führt dazu, dass Sie zwei VIs schreiben müssen, eines im Gemeinschaftsbereich und eines im geschützten Bereich.

F&E halten dies nicht für einen Fehler. Diese Funktionsweise ist von uns beabsichtigt. Wir haben uns dafür entschieden, die Schnittstelle nicht durch einen weiteren neuen Bereich zu verkomplizieren, der „geschützt und gemeinschaftlich“ miteinander kombiniert. Und wir konnten nicht zulassen, dass die Unterklassen pauschal auf die VIs mit Gemeinschaftsrechten zugreifen können, ohne für eine Hintertür im Schutz des Gültigkeitsbereichs zu sorgen. Nur Freunde sollten berechtigt sein, die VIs im Gemeinschaftsbereich aufzurufen. Und wenn ein Programmierer einfach ein neues untergeordnetes VI erstellen und dann das VI im Gemeinschaftsbereich in sein eigenes öffentliches VI einbetten könnte, wäre der Gemeinschaftsbereich überflüssig. Die Freundesliste ist eine endliche, explizite Liste, in der aufgeführt ist, wer dieses VI verwenden darf, so dass ein Benutzer bei einer Änderung des Anschlussfelds dieses VIs genau weiß, welche Aufrufer-VIs eventuell bearbeitet werden müssen. Dabei muss er sich keine Gedanken darüber machen, dass viele andere VIs in der absteigenden Hierarchie das VI ebenfalls verwenden könnten.

Kann ich eine innere Klasse erstellen?
Nicht in dieser Version. Eine Klassenbibliothek kann keine anderen Bibliothekstypen (einfache Bibliothek, XControl oder LabVIEW-Klasse) enthalten. Das ist etwas, das wir in der Zukunft gerne anbieten würden, aber die Priorität hängt von der Nachfrage ab.

Wird die private Vererbung unterstützt?
Jedes Bedienelement eines übergeordneten Typs kann Daten eines beliebigen untergeordneten Typs enthalten. Die Daten, die über eine übergeordnete Verbindung nach unten fließen, können von einem beliebigen untergeordneten Typ sein. Darin liegt der Schlüssel für die Funktionsweise der dynamischen Bindung und warum die OO Ihnen helfen kann, generische Algorithmen zu entwickeln, die benutzerdefinierte Schritte für jeden Klassentyp ausführen. Während der Entwicklung gab ein Beta-Kunde die Rückmeldung, dass sich dieser Umstand nachteilig auf die Stellung von LabVIEW als stark typisierte Sprache auswirke. Er suchte eine Möglichkeit, eine Unterklasse von einer übergeordneten Klasse zu erben und zugleich zu verhindern, dass die Unterklasse eine Typumwandlung auf einen übergeordneten Typ vornimmt. Wenn er auf diese Weise versehentlich eine untergeordnete Verbindung mit einem übergeordneten Anschluss verbindet, würde er eine nicht ausführbare Verbindung erhalten.

Im Grunde genommen fragte der Kunde nach einer Funktion, die in anderen Sprachen als „private Vererbung“ bezeichnet wird. Bei der privaten Vererbung würde eine Klasse zwar immer noch alle Attribute ihrer übergeordneten Klasse erben, aber kein VI außerhalb der Klasse wüsste um diese Vererbung und könnte daher die Typen nicht miteinander verbinden. LabVIEW bietet nur öffentliche Vererbung. Private Vererbung ist ein selten genutztes Konzept der OO-Programmierung, das für unnötige Komplexität sorgen würde. Mit jedem Versuch, dies zu unterstützen, müssten einige schwierige Fragen dazu beantwortet werden, wie die private Vererbung in unserer Sprache funktionieren sollte. Sie müssten manchmal in der Lage sein, untergeordnete Daten mit übergeordneten Anschlüssen zu verbinden, um von der übergeordneten Klasse geerbte Methoden aufrufen zu können. Dadurch würden jedoch untergeordnete Daten innerhalb eines übergeordneten Bedienelements auf dem FP des übergeordneten VIs abgelegt werden. Was geschähe, wenn auf den Wert dieses übergeordneten Bedienelements über einen Eigenschaftsknoten von einem VI zugegriffen werden würde, das nicht zur Klasse gehört? Würde eine Fehlermeldung ausgegeben werden? Oder sollten wir zulassen, dass die Daten des Unterobjekts zurückgegeben werden, obwohl niemand außerhalb der Klasse wissen soll, dass das Unterobjekt und das übergeordnete Objekt in Beziehung zueinander stehen? Solche Fragen erschweren den Umgang mit dem Prinzip der privaten Vererbung. Es ist nicht davon auszugehen, dass diese Funktion irgendwann in LabVIEW enthalten sein wird.

Wird die Mehrfachvererbung unterstützt?
Mehrfachvererbung – mehrere übergeordnete Klassen, die alle Daten und Funktionsweisen von zwei Klassen zusammenführen – ist eine großartige Theorie, die in der praktischen Anwendung für ebenso viele Probleme wie Problemlösungen sorgt. „Ich möchte alle Attribute von A und von B in C zusammengefasst haben“ klingt wirklich gut, bis man anfängt zu überlegen, wie man mit Namenskollisionen umgeht, wie man die Daten zusammensetzt, wie man die Diamantenvererbung auflöst (wo D von B und C erbt, die beide von A erben) und andere unschöne Anwendungsfälle. LabVIEW bietet keine Mehrfachvererbung und das wird sich wahrscheinlich auch in Zukunft nicht ändern. In LabVIEW 2020 wurden LabVIEW-Schnittstellen hinzugefügt, die einen neuen Datentyp darstellen, der ähnlich wie Klassen ist, aber keine Privatdaten enthält. Mit Schnittstellen lassen sich die meisten Anwendungsfälle für Mehrfachvererbung lösen. Weitere Informationen zu Schnittstellen finden Sie in den mitgelieferten Unterlagen zu LabVIEW 2020. Einzelheiten zu ihrer Implementierung lassen sich unter LabVIEW-Schnittstellen nachlesen: Die Hintergrundentscheidungen beim Entwurf

Warum sind in LabVIEW LabVIEW-Klassen in der Schnittstelle von in LV erstellten DLL-Dateien (gemeinsamen Bibliotheken) verboten?  Warum lassen sich keine LabVIEW-Objekte an einen Knoten für den Aufruf externen Bibliotheken übergeben?
Wenn Sie den Application Builder verwenden, um eine DLL (oder eine gemeinsam genutzte Bibliothek auf anderen Plattformen) zu erstellen, lässt LabVIEW nicht zu, dass ein VI aus dieser DLL exportiert wird, wenn im Anschlussfeld des VIs irgendwelche LabVIEW-Klassen verwendet werden. Die Schnittstelle zwischen LabVIEW und externem Programmcode muss aus einfachen Datentypen bestehen.

Wenn Sie sich das speicherinterne Layout einer Klasseninstanz ansehen, werden Sie feststellen, dass darin eine Menge sehr LabVIEW-spezifischer Zeiger und Zuweisungen enthalten sind. Keine andere EXE- oder DLL-Datei hat die Möglichkeit, diese Datenstruktur zu verwenden oder auf die Teile der Klasse zuzugreifen, die zur Unterstützung der dynamischen Bindung oder zur Durchsetzung des klassenspezifischen Gültigkeitsbereichs erforderlich sind. In ähnlicher Weise unterscheidet sich die C++-Implementierung von Klassen von Compiler zu Compiler. Die meisten C++-Benutzer raten aufgrund dieser Varianz davon ab, C++-Klassen in die Schnittstelle von DLLs einzubinden. Es liegt in der Natur von OO, dass die Implementierungsstruktur, die für einen Compiler am besten geeignet ist, bei einem anderen Gerät nicht passt, sodass sie selten miteinander interagieren können.

Kann ich meine Klasse in/aus .NET importieren/exportieren?
[LV2009] Mit LabVIEW 2009 können Sie .NET-Assemblys aus Ihren VIs erstellen. Erstellen Sie dazu eine neue Build-Spezifikation in Ihrem Projekt für „.NET Assembly“. Sie können LabVIEW-Klassen als Teil Ihrer Quell-VIs einbinden. Diese Klassen können in der Assembly-Schnittstelle exportiert und als .NET-Klassen dargestellt werden. Wenn Sie dann versuchen, diese .NET-Assembly in LabVIEW zu verwenden, wird die Klasse als .NET-Referenz angezeigt und auf Referenzebene verarbeitet, so wie es bei jeder anderen Referenz der Fall wäre.

Fazit


Wird LVOOP alle Anwender-VIs revolutionieren und eines Tages als „die attraktivste Komponente von LabVIEW“ bezeichnet werden?
Einige von uns träumen öfter von einer OO-Revolution. Aber ehrlich gesagt ist OO nicht für jeden geeignet. Als Werkzeug ist es Teil einer sich ständig erweiternden Werkzeugpalette von LabVIEW, die dem Anwender zur Verfügung steht. Wenn ein schnelles einzelnes VI zur Durchführung einer Messung benötigt wird, sind LabVIEW-Klassen zu aufwändig. Für umfassende Anwendungen hingegen, ja selbst für kleine Hilfsprogramme, kann die objektorientierte Programmierung helfen, den Programmcode zu strukturieren, die Wartbarkeit zu verbessern und allgemein die Arbeit mit LabVIEW übersichtlicher zu gestalten. In der ersten Version bestanden noch einige Ecken und Kanten, und wir wussten, dass sich nur unsere erfahreneren Kunden hineinwagen würden, um zu sehen, wie weit sie kommen. Im Laufe der Zeit haben wir jedoch festgestellt, dass OO-Funktionen zu einem festen Bestandteil der LabVIEW-Entwicklung geworden sind. Wir haben sogar schon Anwender von LabVIEW erlebt, die bei ihrem allerersten Projekt mit OO begonnen haben, so wie es auch in anderen Programmiersprachen der Fall ist.

Das LVOOP-Team ist weiterhin zuversichtlich, dass die Geschichte mit der „attraktivsten Komponente“ wahr wird. Lassen Sie uns wissen, was Sie davon halten!

Was this information helpful?

Yes

No