.NET für Umsteiger: VB, VC++ und COM im Blick

29.07.2003 von THOMAS WOELFER 
Der Umstieg auf die Programmierung unter .NET ist auch ohne Wechsel zur neuen Sprache C# ohne Weiteres möglich. Allerdings müssen sich Nutzer von Visual Basic und VC++ in ihren Programmiergewohnheiten anpassen. Dieser Artikel zeigt die wichtigsten Änderungen.

Natürlich kann man beim Programmieren für .NET einfach auf die neue .NET-Sprache C# umsteigen. Doch alles bis dato erworbene Wissen komplett zu den Akten zu legen, kann auch nicht der Sinn der Sache sein. Man kann für .NET auch mit Visual Basic und VC++ programmieren - allerdings unter Beachtung einiger Änderungen.

Wer bisher mit VC++ programmiert hat, der hat mit der neuen Version von VC++ nicht unbedingt irgendetwas Neues zu lernen, denn die aktuelle VC++ Version bietet sich in zwei Betriebsmodi an. Entweder programmiert man für die CLR und schreibt "managed" Code, oder man lässt es bleiben und schreibt weiterhin Programme für die Win32 API. Solcher Code wird mit dem Attribut "unmanaged" belegt.

Im Wesentlichen ist "unmanaged" Code mit VC++ nichts anderes als die logische Fortsetzung von VC++ 6.0 - nur eben ohne die CLR und ohne eine Möglichkeit, die .NET-Klassenbibliothek zu verwenden.

Änderungen beim managed Code

Die Dinge ändern sich, wenn man "managed" Code einsetzt: Hier eröffnen sich dem Programmierer die Möglichkeiten der Klassenbibliothek von .NET, deren Methoden und Objekte. Der wesentliche Unterschied ist dabei der, dass man hier auch bei C++ innerhalb des so genannten Garbage Collector von .NET operiert. Im Klartext bedeutet das, dass Objekte nicht länger per eigenem Code freigegeben werden müssen. Das besorgt stattdessen der Garbage Collector.

Das ist praktisch, aber auf bestehende Projekte nicht ohne Weiteres anwendbar. Diese sind natürlich voller Aufrufe von Destruktoren, die die Common Language Runtime nur stören. Das war aber auch den Programmierern bei Microsoft klar, und so bietet .NET die Möglichkeit, "managed" und "unmanaged" Code zu mischen. Man kann also alte Projekte mit neuem Code aufwerten, sofern man einige Punkte beachtet.

Das Wichtigste dabei ist das so genannte "boxing". Dazu muss man die Unterschiede zwischen "managed" und "unmanaged" Code etwas besser verstehen. Bei Ersterem ist praktisch alles ein Objekt, das dynamisch erzeugt und von der Laufzeitumgebung verwaltet wird. Im normalen C++-Code hingegen werden alle Objekte vom Programmierer verwaltet. Außerdem gibt es jede Menge atomarer Typen, die nicht im Heap, sondern auf dem Stack alloziert werden. Für diesen Widerspruch braucht es eine Lösung, um beide Arten von Code zusammenbringen zu können. Diese Lösung trägt den Namen "boxing". Das "boxing" wird mit separaten neuen Schlüsselwörtern vom C++-Compiler unterstützt.

Im Wesentlichen passiert dabei das Folgende: Wird ein Objekt aus der CLR in "unmanaged" Code übergeben, dann wird dazu eine Kopie der Daten des Objekts erzeugt, die nicht von der CLR verwaltet werden. Mit dieser Kopie kann der "unmanaged" Code dann tun, was ihm beliebt. Geht der Weg hingegen in die andere Richtung, so bekommt das Objekt zuvor eine Hülle, die vom "managed" Code aus verwaltet werden kann. Im einen Fall wird also eine "Box" um das Objekt erzeugt, im anderen Fall wird die Hülle zuvor entfernt.

Visual Basic: Eigentlich eine neue Sprache

Ein Visual-Basic-Programmierer erhält in .NET praktisch die gleichen Möglichkeiten wie ein C#-Programmierer - allerdings zu einem Preis, und der ist nicht unerheblich: Die Sprache als solche, die Art und Weise wie Komponenten verwendet werden und so ungefähr alles andere, was mit der Entwicklung von VB-Anwendungen zusammenhängt, hat sich bei Visual Basic .NET verändert. Man programmiert zwar weiterhin mit einer Sprache, die Visual Basic heißt, nur hat diese eben sehr viele Eigenschaften verloren, während neue hinzugekommen sind: Erhebliches Umgewöhnen ist angesagt.

Auf alle Details der Änderungen im Einzelnen einzugehen, würde an dieser Stelle den Rahmen sprengen. Daher soll die folgende Aussage zunächst ausreichen: Sie können auch für .NET weiterhin mit VB arbeiten, allerdings ist es von der Art und Weise, wie Sie bisher mit dieser Sprache gearbeitet haben, abhängig, ob dieser Umstieg Sinn macht. Unter Umständen ist die benötigte Umgewöhnungszeit auch nicht geringer, als wenn Sie gleich von VB6 auf C# umsteigen statt auf VB.NET.

Ein Beispiel für managed Code in VC++

Wie bereits erwähnt, kann man sehr wohl weiterhin in VC++ programmieren - und das auch für die CLR und unter Verwendung der CLR einschließlich der angebotenen Klassen aus dem .NET-Framework. Eine solche Klasse ist zum Beispiel die String-Klasse aus dem Namespace System.

Im Folgenden finden Sie ein Beispielprogramm, das das benötigte Vorgehen dafür beschreibt. Das Programm tut nur wenig Sinnvolles - zeigt aber die dahinter stehenden Konzepte deutlich auf. Dabei wird Folgendes implementiert: Das Beispielprogramm besteht aus zwei Projekten, wobei es sich bei dem einen um eine normale C#-Anwendung handelt, die im Wesentlichen aus einem Fenster mit einem Button und einem Label-Control besteht. Das zweite Projekt enthält eine mit VC++ programmierte Klasse namens CSample. Diese enthält nur eine einzige Methode namens Get(), die einen im Konstruktor der Klasse allozierten String zurückliefert. Dabei wird jedoch kein C++-String (wie zum Beispiel die MFC CString-Klasse) verwendet, sondern die String-Klasse aus der CLR.

Wird nun im C#-Programm der Button gedrückt, erzeugt das Programm eine neue Instanz des CSample-Objekts, erfragt bei der Instanz den Text und trägt das Resultat im Label-Control ein.

Die Implementierung

So einfach das klingen mag und so einfach das in reinem C# ist - bei einer Mischung von C# und C++ sind einige Dinge zu berücksichtigen.

Zunächst einmal erzeugen Sie ein normales C#-Projekt vom Typ "Windows Application". Das Projekt enthält dann automatisch eine Windows-Forms-Klasse, die Sie mit dem GUI-Editor bearbeiten können. Für diese Testanwendung reicht es aus, einen Button und ein Label-Control per Drag-and-Drop auf dem Fenster zu platzieren.

Das war auf Seiten von C# zunächst alles. Im nächsten Schritt legen Sie ein neues Projekt an, das Sie aber in der gleichen Solution (im gleichen Arbeitsblatt) wie das C#-Projekt speichern. Beim neuen Projekt wählen Sie als Projektart "Managed C++ Class Library" - es wird also ein VC++-Projekt erzeugt, das mit "managed" Code arbeitet. Als Projektnamen geben Sie mc_vc an.

Ist das Projekt erzeugt, so enthält es bereits mehrere CPP-Dateien. Die einzig wichtige ist die Datei "mc_vc.cpp". Hier implementieren Sie nun die Klasse "CSample".

Eine solche Implementierung würde mit normalem C++ unter Verwendung der MFC folgendermaßen aussehen:

#include <stdafx.h>
class CSample
{
private:
CString m_str;
public:
CSample()
{
m_str ="hallo welt";
}

CString Get()
{
return m_str;
}
};

Das geht so aber nicht mehr. Die CString-Klasse ist gar nicht die Klasse, die verwendet werden soll. Stattdessen soll ja die String-Klasse aus der .NET-Klassenbibliothek benutzt werden. Das stellt den Programmierer aber vor ein Problem: Woher kommt dann die Klassendefinition, denn schließlich gibt es für die .NET-Klassen keine Header-Files, die man inkludieren könnte. Ohne Prototyp lässt sich aber auch keine Instanz erzeugen: dumme Sache.

Neue Präprozessor-Direktive für VC++

Für dieses Dilemma gibt es aber eine Lösung, und zwar die Präprozessor-Direktive#using. Diese funktioniert ähnlich wie die mit VC++ 5.0 für die einfachere Verwendung von COM-Objekten aus DLLs eingeführte Direktive#import.

Bei #using gibt man den Namen einer .NET-Assembly an - das ist meist eine DLL. Diese Assembly enthält Typinformationen, Meta-Daten und Code. Auf der Basis dieser Informationen baut der Compiler dann den benötigten Prototyp automatisch auf. Die String-Klasse von .NET befindet sich beispielsweise in der Assembly "system.dll". Man benötigt also ein Statement der Form:

#using <system.dll>

Danach können alle Typen aus der system.dll in VC++ verwendet werden.

Namespace für VC++

Als Nächstes braucht man für die zu implementierende Klasse einen Namespace. Namespaces sind für C++ nichts Neues, waren aber bisher bei VC++ eher optional und wurden selten verwendet. Das ist beim Programmieren für .NET grundlegend anders - an Namespaces führt kein Weg vorbei. Im Beispielprojekt wurde der Namespace SAMPLE verwendet. Die zu implementierende Klasse muss dann innerhalb dieses Namespace definiert werden:

namespace SAMPLE
{
// hier kommt die klasse
}

Die zu implementierende Klasse muss mit einem Schlüsselwort versehen werden, das die Sichtbarkeit der Klasse nach außen definiert. Nachdem die Klasse von außen - also vom C#-Code aus - vollständig sichtbar sein soll, ist in diesem Fall das Schlüsselwort public erforderlich.

public class CSample
{
// die klasse kommt hier
};

__gc: Ein neues Schlüsselwort

Auch das ist noch nicht ausreichend: Die Klasse soll ja eine Klasse innerhalb des "managed" Codes sein - das heißt, der Garbage Collector soll sich um Instanzen dieser Klasse kümmern. Diese Tatsache muss dem C++-Compiler ebenfalls mitgeteilt werden, und das geht mit dem neuen Schlüsselwort __gc. Die Klasse sieht dann folgendermaßen aus:

public __gc class CSample
{
// die klasse kommt hier
};

Innerhalb der Klasse selbst passiert zunächst nichts Außergewöhnliches. Es gibt einen public Teil mit dem Konstruktor und der Get() Methode, sowie einen private Teil für die Instanz des String.

Dass sich hier nichts tut, scheint aber nur auf den ersten Blick der Fall zu sein. Die Member-Variable vom Typ "String" muss das folgende Aussehen haben:

String* m_pstr;

Es handelt sich also nicht um eine Instanz vom Typ "String", sondern um einen Zeiger auf einen String. Sie können sogar keine Instanz vom Typ "String" definieren, ein solcher Versuch wird vom Compiler mit einer Fehlermeldung quittiert:

String m_str;

Der Grund dafür ist eben managed Code. In diesem Rahmen müssen alle Objekte dynamisch alloziert werden, Sie arbeiten grundsätzlich mit Zeigern auf Objekte. Würde die String-Instanz nicht dynamisch alloziert, dann könnte sie auch nicht vom Garbage Collector verwaltet werden. Daher lässt der Compiler dies nicht zu. Das ist jedoch gewöhnungsbedürftig - und so gibt es extra eine spezielle Fehlermeldung des Compilers: "Did you forget a *?".

CSample()
{
m_pstr = new String("Hallo from garbage collected vc++");
}

Ebenso ist auch die "Get()"-Methode betroffen - sie liefert schließlich ebenfalls einen Zeiger:

String* Get()
{
return m_pstr;
}

Jetzt unnötig: Ein Destruktor zur Speicherfreigabe

Damit ist die CSample-Klasse abgeschlossen. Es gibt nirgends einen Destruktor, der m_pstr wieder freigibt. Da der String aber dynamisch alloziert wurde, würde man normalerweise noch den folgenden Code erwarten:

~CSample()
{
delete m_pstr;
}

Das ist aber genau einer der Vorteile, die Ihnen der managed Code bietet: Es ist schlicht und ergreifend nicht mehr erforderlich, den String wieder freizugeben. Der Garbage Collector der Laufzeitumgebung kümmert sich um dieses Detail.

Die vollständige Implementierung der Csample-Klasse sieht also folgendermaßen aus:

#using <system.dll>
namespace SAMPLE
{
public __gc class CSample
{
private:
String* m_pstr;
public:
CSample()
{
m_pstr = new String("Hallo from garbage collected vc++");
}

String* Get()
{
return m_pstr;
}
};
}

Auf Grund der Tatsache, dass es sich um ein Projekt mit managed Code handelt, kann die CSample-Klasse ab sofort genau so weiter verwendet werden, wie das bei der bereits verwendeten "String"-Klasse der Fall ist. Dazu braucht man nur noch die vom Projekt erzeugte DLL - ein Header-File oder eine separate Link-Library sind nicht mehr notwendig. Das können Sie gleich selbst ausprobieren. Dazu wechseln Sie zurück in das C#-Projekt.

C# benutzt managed VC++

Im C#-Projekt öffnen Sie Solution-Explorer den Ast "References" unter "Windows Application 1". Dort sehen Sie verschiedene Einträge, die beim Anlegen des Projekts automatisch vorgenommen werden. Einer dieser Einträge trägt den Namen "system" und verweist auf die gleiche system.dll, die Sie schon im VC++-Projekt eingebunden haben, um den Typ "String" verwenden zu können. Wie man sieht, erfolgt diese Einbindung bei C# anders als bei VC++: In VC++ binden Sie die Referenzen auf Assemblies im Quellcode per #using ein, während das bei C# direkt in der Entwicklungsumgebung geht.

Genau eine solche Referenz fügen Sie nun hinzu. Dazu klicken Sie mit der rechten Maustaste auf "References" und wählen dann "Add Reference". Im folgenden Dialog können Sie sich zwischen .NET-Komponenten, COM-Komponenten und Projekten entscheiden. Da die CSample-Klasse aus einem eigenen Projekt stammt, wählen Sie den Reiter "Projects" und erhalten eine Liste aller Projekte aus Ihrer Solution. Wählen Sie dort das C++-Projekt aus und schließen den Dialog.

Das war's: Sie können die CSample-Klasse nun verwenden. Um das auch tatsächlich zu tun, machen Sie einen Doppelklick auf den Button im C#-Programm. Die Entwicklungsumgebung fügt dann einen Eventhandler für Button-Click ein, der den Namen button1_click() trägt.

Hier erzeugen Sie nun eine neue CSample-Instanz und weisen dem "Label"-Control den Text der Instanz zu:

CSample s = new CSample();
this.label1.Text = s.Get();

Auch hier ist kein Destruktor erforderlich, und Sie müssen das Objekt hier ebenfalls nicht selbst wieder zerstören. Mit anderen Worten: Sie können nun auch das C#-Projekt übersetzen und ausprobieren und haben ein Programm, das sich aus VC++-Code und C#-Code zusammensetzt.

Objekte transportieren: Boxen bauen und auspacken

Das alles ist ja ganz nett, aber dummerweise will man nun innerhalb von C++ nicht immer mit Zeigern auf Objekte arbeiten. Bei komplexen Objekten wird das in normalem Code zwar schon immer der Fall gewesen sein, anders ist das aber bei Typen, die ihrer Art nach eher atomarer Natur sind: Das gilt zum Beispiel für Punkte, die wie folgt definiert sein können und für geometrische Operationen verwendet werden:

struct Point
{
double x;
double y;
};

Hier - und in praktisch allen anderen Fällen, bei denen structs zum Zuge kommen, will man nahezu immer erreichen, dass diese Elemente eben keine von der CLR verwalteten Objekte sind. Wie man derlei Dinge im C++-Code verwendet, zeigt das folgende Beispiel.

Grundsätzlich ist es so, dass die CLR zwischen zwei Typen unterscheidet: Ein Element ist entweder ein "Object"-Type und wird als Objekt verwaltet, oder ein "Value"-Type. Letztere werden nicht von der CLR verwaltet, sondern funktionieren wie Variablen, die bei herkömmlichem C auf dem Stack abgelegt werden.

Boxing: Konvertierung zwischen Object- und Value-Typen

Um damit zu arbeiten, ist es aber notwendig, zwischen den beiden Typen "Object" und "Value" konvertieren zu können: Man will einen "Value" manchmal eben doch als Objekt betrachten können - in anderen Fällen will man das nicht.

Dazu dient das "Boxing" und "Unboxing" genannte Verfahren. Ersteres wandelt von "Value" nach "Object" um, indem ein Objekt erzeugt und die Daten des "Value" darin abgelegt werden. Mit diesem Objekt kann man dann weiterarbeiten. Das "Unboxing" kehrt diesen Vorgang wieder um.

Im Beispiel wird hierzu im C++-Code zunächst ein "Value" definiert. Dabei handelt es sich einfach um eine Struktur, die per public öffentlich gemacht wird und per __value als "Value"-Type markiert ist:

public __value struct V
{
double d;
};

Diese Struktur soll nun einmal als "Value" und ein anderes Mal als "Object" betrachtet werden. Dazu wird eine Klasse definiert, mit der man eine solche Struktur manipulieren kann. Die Klasse initialisiert eine Variable vom Typ "V" mit dem Wert "42" und bietet dann verschiedene Zugriffsmöglichkeiten an: Die Variable lässt sich als "Value" oder als "Object" erfragen, außerdem ist es möglich, die Variable als "Object" mit einem neuen Wert zu belegen. Mit anderen Worten: Die Behandlung ist sowohl als Value-Type als auch als Object-Type möglich.

Boxing und Unboxing im Beispiel

public __gc class CBoxSample
{
private:
V m_d;

public:
CBoxSample()
{
m_d.d = 42;
}

Object* GetValueAsObject()
{
return __box( m_d);
}

double GetValue()
{
return m_d.d;
}

void Set( Object* o)
{
m_d = *dynamic_cast< __box V*>(o);
}
};

Der Konstruktor sieht noch völlig normal aus: Das Member m_d.d wird mit dem Wert 42 belegt. Die Methode GetValueAsObject() ist hingegen schon etwas verwirrend. Sie liefert einen Zeiger auf "Object" zurück - dabei handelt es sich um die Basisklasse "Object" der .NET-Framework-Klassenbibliothek. Dazu muss der Wert aber in ein Objekt konvertiert werden, denn schließlich wurde ja zuvor explizit festgelegt, dass es sich bei struct V um einen "Value"-Typ handelt (__value). Das geht mit dem VC++-Schlüsselwort __box.

Die Methode Set() bekommt einen Parameter vom Typ Object* - sie lässt sich also mit einem "Object"-Type aufrufen. Die Methode soll aber das Member m_d neu belegen. Es ist also erforderlich, das Objekt in seine "Value"-Repräsentation zurückzuwandeln. Nun würde man vielleicht ein Schlüsselwort wie __unbox erwarten, das dies leistet: Das gibt es aber nicht. Stattdessen verwendet man normale Cast-Operatoren von C++, um die Umwandlung durchzuführen, und eine abschließende Dereferenzierung.

Innerhalb des C#-Codes kann man das nun ausprobieren. Im Beispiel wurde es mit einem dritten Button und einem entsprechenden Event-Handler gelöst.

private void button3_Click(object sender, System.EventArgs e)
{
CBoxSample b = new CBoxSample();
string s = b.GetValueAsObject().ToString();
MessageBox.Show( s);
MessageBox.Show( b.GetValue().ToString());

V v;
v.d = 50.0;

b.Set( v as object);
s = b.GetValueAsObject().ToString();
MessageBox.Show( s);
MessageBox.Show( b.GetValue().ToString());
}

COM-Komponenten verwenden: Wrapper automatisch erzeugen

Am einfachsten können Sie COM-Objekte wiederverwenden, indem Sie diese Objekte über die Werkzeugleiste von Visual Studio zugänglich machen. Dazu klicken Sie mit der rechten Maustaste auf die "Toolbox" und wählen den Befehl "Customize Toolbox". Daraufhin erscheint eine Dialogbox, die es Ihnen ermöglicht, aus .NET- und COM- Komponenten auszuwählen.

Sie können nun zum Beispiel eine COM-Komponente wie den "Microsoft Webbrowser" auswählen - dazu müssen Sie einfach nur die Option neben der gewünschten Komponente anklicken und den Dialog schließen: Die Komponente erscheint am unteren Rand der Toolbox.

Nun können Sie diese Komponenten aus der Toolbox auf ein Fenster ziehen: Die Entwicklungsumgebung erzeugt dann passende Wrapper-Klassen, die automatisch ins Projekt eingebunden werden. Im C#-Quellcode benutzen Sie einfach die neue Komponente: Im Fall des Webbrowsers zum Beispiel, indem Sie dessen Navigate()-Methode aufrufen.

Eine weitere Klasse in Visual Basic

Abschließend soll das bisherige Beispielprojekt noch um eine Klasse erweitert werden, die in Visual Basic implementiert ist. Dabei gehen wir auch nochmals auf die Namespaces ein, allerdings mit einem alternativen Verfahren.

Bei der Erweiterung des Programms durch VB-Code passiert nichts grundlegend Neues: Das C#-Fenster erhält zunächst einen weiteren Button und ein weiteres Label-Control. Beide ziehen Sie einfach aus der Toolbox auf das Fenster.

Dann legen Sie ein weiteres Projekt an, das Sie in der aktuellen Solution platzieren. Diesmal wählen Sie aber als Projekttyp eine Visual-Basic-Klassenbibliothek aus. Das erzeugte Projekt enthält dann wieder verschiedene Files - aber nur die Datei class1.vb ist davon zunächst von Interesse.

Wenn Sie die Datei öffnen, finden Sie die folgenden Statements vor:

Public Class Class1
End Class

Es liegt zunächst eine Klasse namens Class1 vor: Den Namen der Klasse können Sie natürlich ändern, was Sie für ein normales Projekt wohl auch tun würden. Diese VB-Klasse erweitern Sie nun einfach um eine Funktion namens GetFromVB mit dem folgenden Aussehen:

Public Function GetFromVB()
GetFromVB = "hello from VB"
End Function

Sie müssen hier weder Namespaces beachten noch irgendwelche Referenzen einfügen: Anders als bei VC++ sind die benötigten Referenzen bereits durch die Entwicklungsumgebung eingefügt worden. Der Namespace wird im Beispiel absichtlich nicht gesetzt, damit Sie die Auswirkungen davon sehen können.

Damit ist die Implementierung der - zugegebenermaßen etwas simplen - VB-Klasse abgeschlossen.

VB-Objekte in C# benutzen

Im C#-Projekt gehen Sie dann den bereits bekannten Weg: Mit Hilfe der rechten Maustaste fügen Sie eine neue Referenz auf das soeben übersetzte VB-Projekt zum C#-Projekt hinzu.

Dann doppelklicken Sie auf den neuen Button und landen dadurch im Event-Handler für den entsprechenden Button.

Diese füllen Sie nun aus - und zwar auf eine ähnliche Art wie bereits beim ersten Event-Handler: Sie erzeugen eine Instanz vom Typ der gewünschten Klasse - in diesem Fall Class1 - und tragen dann den gelieferten Text im neuen Label-Control ein. Dabei ist aber Folgendes zu berücksichtigen: Beim VB-Projekt haben Sie keinen Namespace verwendet, und darum kam dort der Default-Namespace zum Zuge. Der hat den gleichen Namen wie das Projekt, also ClassLibrary1. Da Sie sich aber im Code des C#-Projekts befinden und dieses seinerseits innerhalb des Namespace SAMPLE liegt, muss die gewünschte VB-Klasse mit Ihrem kompletten Namen angesprochen werden - und der enthält auch den Namespace:

ClassLibrary1.Class1 c = new ClassLibrary1.Class1();
this.label2.Text = c.GetFromVB().ToString();

Sie wissen nun, was Sie bei dem Versuch erwartet, Komponenten weiterzuverwenden, die Sie mit Ihrer bisherigen Sprache entwickelt haben - und welche neuen Möglichkeiten sich bieten. Ganz egal, ob Sie dabei mit managed VC++, mit VB, oder mit C# arbeiten: Die Nutzung der CLR und der zugehörigen Klassenbibliothek lohnt sich auf jeden Fall. (mha)