ITMAGAZINE C#-Workshop, Teil 2: C#-Objekte im Praxiseinsatz 15. Juni 2001 - Der zweite Teil des C#-Workshops beschäftigt sich mit der Erstellung und Behandlung von Objekten und zeigt, wie die Leistungsfähigkeit des .Net-Frameworks genutzt werden kann. Ein Objekt definiert sich durch Eigenschaften, Methoden und Ereignisse. Ein entscheidender Aspekt der objektorientierten Programmierung ist neben der Vererbung vor allem die Kapselung von Programmlogik. Die Einhaltung dieses Grundsatzes gewährleistet die Erstellung von Objekten, welche dann in einem hohen Masse wiederwendbar sind und sich universell einsetzen lassen. In C# wird die Definition eines Objektes eingeleitet durch das Schlüsselwort class. Eine hierdurch erzeugte Klasse entspricht einem Objekt. Durch Schlüsselwörter, denen class vorangestellt wurde, werden die Sichtbarkeit dieser Klasse innerhalb des aktuellen Projektes und eine Veröffentlichung gesteuert. Eine als public definierte Klasse ist von anderen Programmen instanzierbar und kann entsprechend benutzt werden. Hingegen ist eine als privat gekennzeichnete Klasse weder innerhalb des aktuellen Projektes noch von anderen Programmen verwendbar. Soll eine Klasse innerhalb des aktuellen Projektes verwendet werden, so ist das Schlüsselwort internal voranzustellen. Über weitere Schlüsselwörter kann das Verhalten einer Klasse bei der Vererbung gesteuert werden. Soll eine Klasse lediglich eine Struktur definieren, die von vererbten Objekten genutzt wird, so besteht die Möglichkeit, eine Klasse als abstract zu kennzeichnen. Eine so definierte Klasse muss in jedem Fall vererbt werden, sie kann nicht direkt in einem Programm instanziert werden. Der Einsatz eines solchen Konstruktes erlaubt beispielsweise die Definition eines Standardobjektleistungsumfanges, der dann individuell gefüllt werden kann. Es gibt auch Fälle, in denen vermieden werden soll, dass eine Klasse abgeleitet wird. Durch Einsatz des Schlüsselwortes sealed wird die Ableitung verboten und die Klasse damit als endgültig festgelegt. Eigenschaften festlegen Nach der Definition des Objektes, der Sichtbarkeit und des Vererbungsverhaltens gilt nun die Betrachtung der Definition von Eigenschaften innerhalb einer Klasse. Es ist zwar generell möglich, eine Variable innerhalb der Klasse als public zu definieren. Dies entspricht aber nicht dem Gedanken der Kapselung. Durch die Kapselung von Eigenschaften lässt sich auch das Verhalten beim Zugriff auf diese Eigenschaften im Detail steuern. Zudem könnte es notwendig sein, bei der Zuweisung von Eigenschaften entsprechende Plausibilitätsprüfungen durchzuführen. Auch die Abfrage von Eigenschaften könnte dynamisch aufgrund von anderen Eigenschaften unterschiedliche Werte zurückgeben. Ein solches Konstrukt zur Definition einer Eigenschaft kapselt prinzipiell den Zugriff auf eine private Variable. Das folgende Beispiel zeigt die Realisierung einer Eigenschaft Nachname in einem öffentlichen Objekt Person. Dabei wird der Name bei der Zuweisung immer auf 30 Zeichen gekürzt. namespace Adressen { using System; public class Person { private string_ mstr_Nachname=""; public string Nachname { set { mstr_Nachname =_ value.Substring(1,30); } get { return mstr_Nachname; } } } } //Codezeilen, die aus Layout//technischen Gründen umbrochen //werden mussten, werden an der //betreffenden Stelle mit einem //Unterstrich gekennzeichnet. Auf die Deklaration der Klasse erfolgt die Definition einer innerhalb dieser Klasse privaten Variable mstr_Nachname. Die Zeile public string Nachname leitet die Definition einer Eigenschaft ein. Der set-Block kommt zur Ausführung, wenn dieser Eigenschaft ein Wert zugewiesen wird. Dabei steht der Wert über den speziellen Parameter value zur Verfügung. Der get-Block wird bei der Abfrage dieser Eigenschaft durchlaufen. Hier erfolgt die Rückgabe des Wertes der Eigenschaft über das Schlüsselwort return. namespace Adressen { using System; public class Test { public void Main() { //Variable vom Typ //definieren Person objPerson; //objPerson instanzieren objPerson = new Person(); //Eigenschaft Nachname //mit Wert versorgen objPerson.Nachname =_ "Groth"; //Eigenschaft Nachname //auslesen Console.WriteLine_ ("Nachname:,objPerson._ Nachname); } } } Oftmals kommt es vor, dass eine Eigenschaft schreibgeschützt sein muss, beispielsweise eine Status-Eigenschaft. Um dieses zu erreichen, genügt es, den entsprechenden set-Block wegzulassen. Danach erzeugt eine Zuweisung dieser Eigenschaft einen Laufzeitfehler. private string mstr_ID=""; public string ID { get { return mstr_ID; } } Methoden definieren Die Definition von Methoden entspricht dem unter C# üblichen Standard zur Definition von Prozeduren und Funktion. Eine Prozedur wird mit dem vorgestellten Schlüsselwort void versehen. Typisch für ein Objekt Person wäre die Implementierung einer Methode Update, welche die Daten in einer Datenbank speichert. public void Update() { //Code für das Update //der Datenbank } Bei einer Funktion wird dem Namen der Funktion der Rückgabetyp vorangestellt. Als Beispiel bietet sich die Implementierung der Methode ToString() an. Viele der Basisklassen des .Net-Frameworks stellen diese Methode zur Verfügung, um den Inhalt des Objektes als Zeichenfolge formatiert ausgeben zu können. Hierbei wurde die exemplarisch verwandte Klasse Person um die Eigenschaften Anrede und Vorname erweitert. override public string ToString() { return this.Anrede_ + " " + this.Vorname_ + " " + this.Nachname; } Oftmals stellt es ein Problem dar, dass Funktionen nur genau einen Wert zurückliefern können. C# stellt dazu die Schlüsselwörter ref und out zur Verfügung, die einem Parameter vorgestellt werden. Während ref mit einer Referenz auf die dort übergebene Variable arbeitet, bietet out zusätzlich den Vorteil, mit nicht initialisierten Variablen arbeiten zu können. Prinzipiell entspricht out aber der Arbeit mit einer Referenz. Um dem Prinzip der Kapselung auch bei Eigenschaften und Methoden gerecht zu werden, ist hier der Einsatz von Schlüsselwörtern zur Steuerung der Sichtbarkeit notwendig Entsprechend der Definition einer Klasse veröffentlicht public die entsprechende Eigenschaft oder Methode, private hingegen macht sie nur für die aktuelle Klasse zugänglich. Soll der Zugriff nur Innerhalb des aktuellen Projektes erfolgen, so ist internal zu wählen. Zusätzlich kann eine Eigenschaft oder Methode aber auch in ihrem Sichtbarkeitsbereich als protected definiert werden. Dies hat zur Folge, dass die Sichtbarkeit nur innerhalb von abgeleiteten Klassen gegeben ist. So können den "Kunden" des Objektes eigentlich private Funktionalitäten exklusiv zur Verfügung gestellt werden. können den "Kunden" des Objektes eigentlich private Funktionalitäten exklusiv zur Verfügung gestellt werden. Zusätzlich kann noch die Schlüsselwortkombination internal protected verwandt werden, diese beschränkt die Möglichkeiten von protected auf das aktuelle Projekt. Besonderheiten mit abstract Neben der Festlegung, in welcher Form Eigenschaften und Methoden sichtbar sein sollen, ist natürlich auch die Steuerung des Objektes bei der Vererbung wichtig. Dabei ist eine Besonderheit bei der Verwendung des Schlüsselwortes abstract zu bedenken. Da hier eine Implementation in der abgeleiteten Klasse notwendig ist, muss eine Vererbung in jedem Fall stattfinden. Es ist denn auch nicht möglich, eine Klasse direkt zu instanzieren, die in irgendeiner Form eine abstract-Definition enthält. Faktisch erhält somit die gesamte Klasse den festgelegten Status abstract. public abstract class DataEntry { public abstract void AddNew(); public abstract void Edit(); public abstract void Delete(); public abstract void Read(); } Dieses Beispiel definiert die Struktur für einen Typ von Klasse, über welche die Erstellung von Objekten des Typs Datenbankeinträge möglich wird. In den abgeleiteten Klassen müssen die einzelnen Methoden dann entsprechend mit Code zum Datenbankzugriff implementiert werden. Statische Methoden und Eigenschaften Ein weiteres Konzept der objektorientierten Programmierung unter C# sind statische Methoden und Eigenschaften, die vorwiegend zum Einsatz kommen. Auch wenn es sogar möglich ist, Variablen statisch zu definieren, so ist eine Kapselung über die Erstellung einer Eigenschaft dennoch vorzuziehen. Durch die Verwendung des Schlüsselwortes static wird eine solche Eigenschaft unabhängig von allen Instanzen. Ausschlaggebend ist immer die Basisklasse. Das folgende Beispiel realisiert einen Zähler, der angibt, wie oft eine Instanz der Klasse Person erstellt wurde. Der Code zum Inkrementieren dieses Zählers befindet sich im Konstruktor der Klasse, welcher im folgenden noch detailliert vorgestellt wird. private int static_ mint_InstCount=0; public static int InstCount { set { mint_InstCount = value; } get { return mint_InstCount; } } Virtuelle Funktionen dienen zur Optimierung von Methoden unter dem Schlüsselwort virtuell. Virtuell bedeutet hierbei, dass der Compiler bei abgeleiteten Klassen den tatsächlichen Objekttyp ermittelt und darauf basierend die am besten geeignete Funktion aufruft. Eine so definierte Methode kann überschrieben werden, ohne Gefahr zu laufen, dass die in der Basisklasse definierte Funktion aufgerufen wird. Im folgenden wird die Methode Update der Klasse Person nun als virtuell definiert, um sie später in einem auf Person abgeleiteten Objekt überschreiben zu können. virtual public void Update() { //Code für Update auf //die Datenbank einfügen } Erstellungsroutinen einsetzen Als eine komfortable und sichere Erstellung von Objekten zur Laufzeit ist der Einsatz von Erstellungsroutinen oder Konstruktoren gedacht. Eine solche Erstellungsroutine wird mit dem selben Namen wie die Klasse versehen und kann über eine beliebige Anzahl von Parametern verfügen. Für die Klasse Person existiert eine Erstellungsroutine, über die alle wichtigen Eigenschaften sofort bei der Erstellung einer Instanz des Objektes gesetzt werden können. public class Person { public Person(string_ Anrede,string Vorname,_ string Nachname) { this.Anrede = Anrede; this.Vorname = Vorname; this.Nachname = Nachname; //Instanzenzähler erhöhen mint_InstCount++; } ... Dieser Quelltext macht von dem Schlüsselwort this Gebrauch, das einen Verweis auf die aktuelle Klasse enthält. So können die Eigenschaften der eigenen Klasse angesprochen werden. An dieser Stelle macht der Einsatz eines weiteren Konzeptes der objektorientierten Programmierung Sinn: Durch Überladung kann eine weitere Erstellungsroutine unter dem selben Namen im Quelltext vorhanden sein, es müssen sich lediglich die Parameter unterscheiden. Wird eine so überladene Funktion aufgerufen, wählt der Compiler die geeignete Funktion durch unterscheiden. Wird eine so überladene Funktion aufgerufen, wählt der Compiler die geeignete Funktion durch Abgleich der Parameter im Aufruf mit den für die Methode definierten Parametern. Die zuerst erstellte Routine lässt lediglich das Füllen des Objektes mit neuen Daten zu. Soll eine Person aber beispielsweise direkt aus der Datenbank über eine ID abgerufen werden, so könnte eine überladene Erstellungsroutine den folgenden Aufbau haben. public class Person { public Person(string_ Anrede,string Vorname,_ string Nachname) { this.Anrede = Anrede; this.Vorname = Vorname; this.Nachname = Nachname; //Instanzenzähler erhöhen mint_InstCount++; } public Person(string ID) { mstr_ID = ID; //Instanzenzähler erhöhen mint_InstCount++; //Datenbankzugriff über ID //hier programmieren } } Bei der Instanzierung des Objektes Person kann nun wahlweise entschieden werden, ob eine neue Person angelegt werden soll oder ob auf eine vorhandene Person über die ID zugegriffen wird. Dies ermöglicht eine sehr effiziente Instanzierung von Objekten. Der folgende Ausschnitt aus einem Quelltext zeigt beide Varianten. public class Test { public void Main() { //Variable vom Typ //Person definieren Person objPerson; //2.Variable vom Typ //Person definieren Person objPerson2; //Instanz objPerson //erstellen objPerson = new_ Person("Herr",_ "Frank","Groth"); //Instanz objPerson2 //durch überladene //Erstellungsroutine objPerson2 = new_ Person("ID1"); } } Die Erzeugung von Klassen Nachdem die Basiskonzepte der Vererbung unter C# erläutert wurden, soll nun auf Basis der Klasse Person eine spezielle Klasse erzeugt werden. Hierzu wählen wir einen Angestellten. Dieser verfügt im Beispiel als weitere Eigenschaft über ein Gehalt. public class_ Angestellter:Person { private double mdbl_Gehalt = 0; public Angestellter(string_ Anrede,string Vorname,string_ Nachname,double Gehalt)_ :base(Anrede,Vorname,Nachname) { { this.Gehalt = Gehalt; } public double Gehalt { set { mdbl_Gehalt = value; } get { return mdbl_Gehalt; } } override public void Update() { //spezieller Code, um einen //Angestellten in der //Datenbank zu erfassen } override public string_ ToString() { return base.ToString()+_ " Gehalt: " +_ this.Gehalt.ToString(); } } } In der Definition der Klasse mit dem Namen Angestellter wird durch den Doppelpunkt und den darauf folgenden Namen der Klasse Person angegeben, dass die Klasse Angestellter eine Vererbung der Klasse Person ist. Damit stehen alle entsprechend definierten Eigenschaften und Methoden der Klasse Person auch als Angestellter zur Verfügung. Da diese Klasse über eine zusätzliche Eigenschaft Gehalt verfügt, muss die Erstellungsroutine angepasst werden. Dabei wird die Erstellungsroutine der Klasse Person genutzt. Das Schlüsselwort base, welches ebenfalls durch einen Doppelpunkt getrennt auf die Definition folgt, ruft die entsprechende Erstellungsroutine der Klasse Person auf. Das Gehalt ist nur bei einem Angestellten definiert, also ist eine entsprechende Eigenschaft zu implementieren. Das Gehalt sollte aus der Erstellungsroutine direkt mit angesprochen werden können. Da die Datenbankstruktur eines Angestellten sich von einer Person unterscheidet, muss die Methode Update ebenfalls angepasst werden. Diese wurde in der Personenklasse als virtuell definiert und kann nun mit override überschrieben und angepasst werden. Dasselbe gilt für die Methode ToString. Diese verwendet über base.ToString() die in der Klasse Person bereits implementierte Methode ToString und hängt lediglich das Gehalt an. C# und das .Net-Framework Im ersten Teil dieses Workshops wurde bereits auf die Ausrichtung von C# auf die Erstellung von Komponenten hingewiesen. Im folgenden soll nun eine solche Komponente erstellt werden. Diese Komponente basiert auf der bereits vorgestellten Klasse Person. Zuerst wird eine zusätzliche Eigenschaft Email implementiert und die Methode ToString um die Ausgabe der in Klammern gesetzten Eigenschaft Email erweitert. Als überladene Erstellungsroutine wurde bereits eine Methode mit dem Parameter vorgesehen. Diese soll auf eine Datenbanktabelle zugreifen, dort den entsprechenden Datensatz anhand der ID finden und die Eigenschaften der Klasse mit den gelesenen Werten füllen. Unter .Net enthält das Framework eine spezielle Klassenbibliothek mit dem Namen ADO.Net. ADO.Net ist die auf .Net abgestimmte Weiterentwicklung der allgemein bekannten Active Data Objects, die heutzutage den Standard beim Datenbankzugriff unter Windows darstellen. Da das gesamte .Net-Framework sich über Klassenbibliotheken präsentiert, ist die Nutzung entsprechender Funktionalitäten sehr einfach. So gibt es unter ADO.Net einige spezielle Objekte, welche sich gezielt mit der Anbindung an Microsoft SQL Server befassen. Diese werden im folgenden bei der Kapselung des Datenbankzugriffes benutzt. Vor der Nutzung von ADO.Net muss der entsprechende Namespace dem aktuellen Projekt bekannt sein. Dazu sollten folgende using-Zeilen vorhanden sein. Im folgenden wird der erweiterte Quelltext für die Routine Person gezeigt. Es werden drei Objekte aus ADO benötigt, um die in SQL Server vorhandene Tabelle "Personen" in der Datenbank "Adressen" anzusprechen zu können. SQLConnection zur Verbindung mit der Datenbank, SQLCommand zur Aufnahme der Select-Anweisung und ein SQLDataReader zum Lesen der Daten. Dieser verhält sich nur vorwärts lesend. Über den Aufruf der Methode Read wird der Datenbank-Cursor weiterbewegt. Da hier nur mit maximal einem Datensatz gerechnet werden kann, reicht eine einfache if-Bedingung. Eine Datenbankanbindung ist generell sehr fehlerträchtig, also muss eine entsprechende Ausnahmebehandlung implementiert werden. Die Methode macht keine eigene Fehlerbehandlung, sondern löst aufgetretene Fehler über throw neu aus. Die Fehlerbehandlung sollte dann in dem aufrufenden Programm stattfinden. Dieses Vorgehen macht diese Komponente unempfindlich gegen Probleme. Abschliessend wird die gesamte Klasse dann als adressen.dll in Form einer Assemblierung kompiliert und kann über den Namespace Adressen weiterverwendet werden. Um die erstellte Komponente über using Adressen einzusetzen, wird eine klassische Windows-Anwendung erstellt. Das .Net-Framework liefert über WinForms einen Satz von Bibliotheken, die darauf abgestimmt sind. Das im untenstehenden Bild abgebildete Formular hat die Aufgabe, nach Eingabe einer ID den zugehörigen Personentext anzuzeigen. Ein direkter Datenbankaufruf aus dem Formular heraus ist nicht gewollt, dafür wurde die Komponente Adressen entwickelt. Die durchgängig objektorientierte Struktur des .Net-Frameworks wird bereits bei der Definition der Formularklasse deutlich. Diese leitet sich nämlich direkt von System.WinForms.Form ab. Danach folgt für alle Steuerelemente eine Variablendefinition und in der Erstellungsroutine PersonAnzeigen wird die Initialisierung aller Controls und des Formulars aufgerufen. Aus Gründen der Übersichtlichkeit wurde der hier notwendige Code ausgeblendet. Er kann aber jederzeit über den WinForms-Designer neu erstellt werden. Für die Funktionalität wichtig ist die Zuweisung einer Ereignisprozedur beim Klicken auf den Button Lesen. In dieser Prozedur wird ein Personenobjekt instanziert, welches über den Namespace Adressen zur Verfügung steht. Bei der Erstellung wird die eingegebene ID übergeben. Dies führt dann zu einem Datenbankzugriff in der vorher erstellten Komponente. Durch den folgenden Aufruf der Methode ToString des Objektes Person wird dann der Inhalt des Objektes objPerson angezeigt. Da das Objekt Person alle Fehlermeldungen weitergibt, ist hier eine entsprechende Fehlerbehandlung notwendig. Abschliessend erfolgt noch die Definition einer Methode Main. Diese dient als Startpunkt der WinForms-Anwendung, indem sie eine Instanz des Formulars erstellt. Die Weiterentwicklung von ASP Unter .Net hat die ASP-Technologie mit ASP.Net sicher den grössten Fortschritt gemacht. So ist es jetzt unter anderem möglich, serverseitig auf Ereignisse zu reagieren. Zudem erfolgt die Erstellung solcher Anwendungen nun objektorientiert und nimmt eine klare Trennung zwischen dem Programmcode und dem Layout vor. Dabei ist ASP.Net auf die Nutzung von HTML 3.2 ausgerichtet. Dieses gewährleistet eine grösstmögliche Browser-Unabhängigkeit. Die Möglichkeit, auf Ereignisse reagieren zu können, erlaubt das bereits als Winforms-Anwendung realisierte Programm zur Nutzung der Komponente Adressen auch als Web-Anwendung zu erstellen. Dabei ist der Programmcode, der durch einen Klick auf den Button Lesen ausgelöst wird, identisch mit der WinForms-Anwendung. mit der WinForms-Anwendung. Der zugehörige HTML-Code ist hier nicht abgebildet. Das Ereignis Page_Load, welches beim Aufruf der Seite ausgelöst wird, dient in diesem Fall zur Ausgabe einer Kopfzeile. Mit ASP.Net ist die Erstellung von interaktiven, komponentenbasierenden Anwendungen einfach geworden. ASP-Anwendungen werden kompiliert und erzielen somit eine hohe Performance. Mit C# ist nun erstmals ein Sprache der C-Familie für die Entwicklung solcher Anwendungen verfügbar. Fazit In diesem Workshop konnten weder alle Möglichkeiten von C# aufgezeigt, noch die Mächtigkeit des .Net-Frameworks in Verbindung mit C# erschöpfend behandelt werden. Es sollte vielmehr Lust geweckt werden, sich einmal C# anzusehen. Wer bereits C++ kennt, wird wenig Probleme haben, auch Java ist nicht die ganz grosse Hürde. Wer sich mit Visual Basic beschäftigt, wird sich unter .Net sowieso mit einem neuen Sprachumfang auseinandersetzen müssen. Vieles in Visual Basic.Net gleicht C#. Für die Erstellung von Anwendungen und Komponenten für .Net ist C# auf jeden Fall eine absolute Empfehlung. Copyright by Swiss IT Media 2017