209 110 14MB
German Pages 1076
Programmer's Choice
Michael Kofler
Visual Basic .NET Grundlagen, Programmiertechniken, Windows-Programmierung
An imprint of Pearson Education München • Boston • San Francisco • Harlow, England Don Mills, Ontario • Sydney • Mexico City Madrid • Amsterdam
Die Deutsche Bibliothek – CIP-Einheitsaufnahme Ein Titeldatensatz für diese Publikation ist bei Der Deutschen Bibliothek erhältlich.
Die Informationen in diesem Buch werden ohne Rücksicht auf einen eventuellen Patentschutz veröffentlicht. Warennamen werden ohne Gewährleistung der freien Verwendbarkeit benutzt. Bei der Zusammenstellung von Texten und Abbildungen wurde mit größter Sorgfalt vorgegangen. Trotzdem können Fehler nicht vollständig ausgeschlossen werden. Verlag, Herausgeber und Autor können für fehlerhafte Angaben und deren Folgen weder juristische Verantwortung noch irgendeine Haftung übernehmen. Für Verbesserungsvorschläge und Hinweise auf Fehler sind Verlag, Herausgeber und Autor dankbar. Alle Rechte vorbehalten, auch die der fotomechanischen Wiedergabe und Speicherung in elektronischen Medien. Die gewerbliche Nutzung der in diesem Produkt gezeigten Modelle und Arbeiten ist nicht zulässig. Fast alle Hardware- und Softwarebezeichnungen, die in diesem Buch erwähnt werden, sind gleichzeitig eingetragene Warenzeichen oder sollten als solche betrachtet werden. Umwelthinweis: Dieses Buch wurde auf chlorfrei gebleichtem Papier gedruckt. Die Einschrumpffolie – zum Schutz vor Verschmutzung – ist aus umweltverträglichem und recyclingfähigem PE-Material.
10 9 8 7 6 5 4 3 2 1 04 03 02 ISBN 3-8273-1982-X © 2002 by Addison-Wesley Verlag, ein Imprint der Pearson Education Deutschland GmbH Martin-Kollar-Straße 10–12, D-81829 München/Germany – Alle Rechte vorbehalten Einbandgestaltung: Christine Rechl, München Titelbild: Abutilon. © Karl Blossfeldt Archiv – Ann und Jürgen Wilde, Zülpich / VG Bild-Kunst Bonn, 2002 Lektorat: Irmgard Wagner, Planegg, [email protected] Korrektorat: Andrea Stumpf, München Satz: Michael Kofler, Graz, www.kofler.cc Druck und Verarbeitung: Bercker, Kevelaer Printed in Germany
Inhaltsübersicht Vorwort Formales
19 23
I
Einführung
1 2 3
Hello World Das .NET-Universum Von VB6 zu VB.NET
II
Grundlagen
4 5 6 7
Variablen- und Objektverwaltung Prozedurale Programmierung Klassenbibliotheken und Objekte anwenden Objektorientierte Programmierung
III
Programmiertechniken
8 9 10 11 12
Zahlen, Zeichenketten, Datum und Uhrzeit Aufzählungen (Arrays, Collections) Dateien und Verzeichnisse Fehlersuche und Fehlerabsicherung Spezialthemen
IV
Windows-Programmierung
557
13 14 15 16 17 18
Windows.Forms – Einführung Steuerelemente Gestaltung von Benutzeroberflächen Grafikprogrammierung (GDI+) Drucken Weitergabe von Windows-Programmen (Setup.exe)
559 575 709 831 959 1003
Anhang A B C D E
Abkürzungsverzeichnis Glossar Dateikennungen Inhalt der CD-ROM Quellenverzeichnis Stichwortverzeichnis
27 29 49 77
113 115 161 185 211
287 289 355 383 471 505
1023 1025 1029 1035 1037 1039 1041
Inhaltsverzeichnis Vorwort
19
Formales
23
I
EINFÜHRUNG
27
1
Hello World
29
1.1 1.2 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7
Hello World (Konsolenversion) Hello World (Windows-Version) Bedienung der Entwicklungsumgebung Layout der Entwicklungsumgebung Menüs und Symbolleisten Tastenkürzel Online-Hilfe Codeeingabe Befehlsfenster Defaulteinstellungen für neue Projekte
30 37 41 41 42 43 43 44 45 47
2
Das .NET-Universum
49
2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.7.1 2.7.2 2.7.3
Wozu .NET? Das .NET-Framework Architektur Sicherheitsmechanismen .NET und das Internet Programmiersprachen (C# versus VB.NET) Entwicklungsumgebungen Microsoft-Entwicklungsumgebungen SharpDevelop Ohne Entwicklungsumgebung arbeiten
50 53 55 61 65 67 69 70 71 74
3
Von VB6 zu VB.NET
77
3.1 3.2 3.2.1 3.2.2 3.2.3 3.2.4 3.2.5
Fundamentale Neuerungen in VB.NET Unterschiede zwischen VB6 und VB.NET Variablenverwaltung Sprachsyntax Bearbeitung von Zahlen und Zeichenketten Windows-Anwendungen und Steuerelemente Grafikprogrammierung und Drucken
78 79 80 84 86 87 100
8
Inhaltsverzeichnis
3.2.6 3.2.7 3.2.8 3.2.9 3.3
Umgang mit Verzeichnissen und Dateien Fehlerabsicherung und Debugging Datenbank- und Internetprogrammierung Sonstiges Der Migrationsassistent
II
GRUNDLAGEN
4
Variablen- und Objektverwaltung
4.1 4.1.1 4.1.2 4.1.3 4.1.4 4.1.5 4.2 4.2.1 4.2.2 4.2.3 4.2.4 4.2.5 4.2.6 4.3 4.4 4.4.1 4.4.2 4.4.3 4.4.4 4.5 4.5.1 4.5.2 4.5.3 4.6 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5
Umgang mit Variablen Deklaration von Variablen Objektvariablen Variablenzuweisungen Option Explicit und Option Strict Syntaxzusammenfassung Variablentypen Ganze Zahlen (Byte, Short, Integer, Long) Fließ- und Festkommazahlen (Single, Double, Decimal) Datum und Uhrzeit (Date) Zeichenketten (String) Objekte Weitere .NET-Datentypen Konstanten Enum-Aufzählungen Syntax und Anwendung Enum-Kombinationen (Flags) Interna (System.Enum-Klasse) Syntaxzusammenfassung Felder Syntax und Anwendung Interna und Programmiertechniken (System.Array-Klasse) Syntaxzusammenfassung Interna der Variablenverwaltung Speicherverwaltung Garbage collection Speicherbedarf Variablen- bzw. Objekttyp feststellen (GetType) Variablen- und Objektkonvertierung (casting)
101 101 103 106 109
113 115 116 116 119 123 125 126 127 127 128 129 129 130 131 132 134 134 135 136 138 139 139 141 145 146 147 148 151 153 157
Inhaltsverzeichnis
9
5
Prozedurale Programmierung
5.1 5.1.1 5.1.2 5.1.3 5.1.4 5.2 5.2.1 5.2.2 5.2.3 5.2.4 5.3 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.4
Verzweigungen (Abfragen) If-Then-Else Select-Case IIf, Choose, Switch Syntaxzusammenfassung Schleifen For-Next-Schleifen For-Each-Schleifen Do-Loop-Schleifen Syntaxzusammenfassung Prozeduren und Funktionen Syntax Lokale und statische Variablen Rekursion Parameterliste Syntaxzusammenfassung Operatoren
161
6
Klassenbibliotheken und Objekte anwenden
6.1 6.1.1 6.1.2 6.1.3 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.3 6.3.1 6.3.2 6.3.3
Schnelleinstieg Miniglossar Beispiel 1 – Textdatei erzeugen und löschen Beispiel 2 – Dateiereignisse empfangen Verwendung der .NET-Bibliotheken Verweise auf Bibliotheken einrichten Klassennamen verkürzen mit Imports Das System-Wirrwarr Shared- und Instance-Klassenmitglieder Objektbrowser Tipps zur Bedienung Deklaration von Schlüsselwörtern Objektbrowser-Icons
186 186 187 190 193 193 194 197 198 203 203 205 208
7
Objektorientierte Programmierung
211
7.1 7.2 7.2.1 7.2.2 7.2.3 7.2.4 7.2.5
Elemente eines Programms Klassen, Module, Strukturen Klassen Klassenvariablen und -konstanten (fields) Methoden Eigenschaften Shared-Klassenmitglieder
162 162 162 163 164 165 165 166 166 167 167 167 170 172 173 180 181
185
212 217 217 220 222 230 236
10
Inhaltsverzeichnis
7.3 7.3.1 7.3.2 7.4 7.4.1 7.4.2 7.4.3 7.5 7.5.1 7.5.2 7.6 7.6.1 7.6.2 7.6.3 7.7 7.8 7.9 7.9.1 7.9.2 7.10
Module und Strukturen Module Strukturen Vererbung Syntax Basisklasse erweitern und ändern LinkedList-Beispiel Schnittstellen (interfaces) Syntax und Anwendung IDisposable-Schnittstelle Ereignisse und Delegates Ereignisse Delegates Delegates und Ereignisse kombinieren Attribute Namensräume Gültigkeitsbereiche (scope) Gültigkeitsbereich definieren Defaultgültigkeit Syntaxzusammenfassung
III
PROGRAMMIERTECHNIKEN
8
Zahlen, Zeichenketten, Datum und Uhrzeit
8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.1.5 8.1.6 8.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.2.7 8.3 8.3.1
Zahlen Notation von Zahlen Rundungsfehler bei Fließkommazahlen Division durch null und der Wert unendlich Arithmetische Funktionen Zahlen runden und andere Funktionen Zufallszahlen Zeichenketten Grundlagen Methoden zur Bearbeitung von Zeichenketten Vergleich von Zeichenketten Interna Unicode, Eurozeichen Zeichenketten effizient zusammensetzen (StringBuilder) Syntaxzusammenfassung Datum und Uhrzeit Methoden zur Bearbeitung von Daten und Zeiten
239 239 242 245 246 248 252 256 257 259 262 263 267 270 272 274 277 277 279 281
287 289 290 290 291 292 293 294 296 298 298 301 305 308 309 312 315 319 321
Inhaltsverzeichnis
11
8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2
Beispiele Syntaxzusammenfassung Konvertierung zwischen Datentypen Automatische und manuelle Konvertierung Konvertierungsfunktionen Spezialmethoden .NET-Formatierungsmethoden .NET-Formatierungsgrundlagen Zahlen formatieren Daten und Zeiten formatieren VB-Formatierungsmethoden Zahlen formatieren Daten und Zeiten formatieren
326 328 331 331 334 338 339 340 343 347 349 350 352
9
Aufzählungen (Arrays, Collections)
355
9.1 9.2 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2
Einführung Klassen- und Schnittstellenüberblick Programmiertechniken Elemente einer Aufzählung einzeln löschen Elemente einer Aufzählungen sortieren Schlüssel einer Aufzählung sortieren Datenaustausch zwischen Aufzählungsobjekten Multithreading Syntaxzusammenfassung Schnittstellen Klassen
10
Dateien und Verzeichnisse
10.1 10.2 10.3 10.3.1 10.3.2 10.3.3 10.3.4 10.3.5 10.3.6 10.3.7 10.3.8 10.4 10.4.1 10.4.2 10.5
Einführung und Überblick Klassen des System.IO-Namensraums Laufwerke, Verzeichnisse, Dateien Informationen über Verzeichnisse und Dateien ermitteln Alle Dateien und Unterverzeichnisse verarbeiten Manipulation von Dateien und Verzeichnissen Spezielle Verzeichnisse, Dateien und Laufwerke ermitteln Bearbeitung von Datei- und Verzeichnisnamen Beispiel – Backup automatisieren Beispiel – Verzeichnisse synchronisieren Syntaxzusammenfassung Standarddialoge Dateiauswahl Verzeichnisauswahl Textdateien lesen und schreiben
356 359 364 365 366 371 373 376 378 378 380
383 384 386 389 390 391 397 402 405 406 407 409 416 416 419 421
12
Inhaltsverzeichnis
10.5.1 10.5.2 10.5.3 10.5.4 10.5.5 10.5.6 10.5.7 10.6 10.6.1 10.6.2 10.6.3 10.6.4 10.6.5 10.7 10.7.1 10.7.2 10.7.3 10.8 10.9 10.9.1 10.9.2 10.9.3 10.9.4 10.10
Codierung von Textdateien Textdateien lesen (StreamReader) Textdateien schreiben (StreamWriter) Beispiel – Textdatei erstellen und lesen Beispiel – Textcodierung ändern Zeichenketten lesen und schreiben (StringReader und StringWriter) Syntaxzusammenfassung Binärdateien lesen und schreiben FileStream BufferedStream (FileStream beschleunigen) MemoryStream (Streams im Arbeitsspeicher) BinaryReader und -Writer (Variablen binär speichern) Syntaxzusammenfassung Asynchroner Zugriff auf Dateien Programmiertechniken Beispiel – Vergleich synchron/asynchron Beispiel – Asynchroner Callback-Aufruf Verzeichnis überwachen Serialisierung Grundlagen Beispiel – String-Feld serialisieren Beispiel – Objekte eigener Klassen serialisieren Beispiel – LinkedList serialisieren IO-Fehler
422 423 427 429 431 432 433 435 436 441 443 444 446 449 450 452 454 456 458 459 463 465 467 468
11
Fehlersuche und Fehlerabsicherung
471
11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.2.3 11.2.4
Fehlerabsicherung Ausnahmen (exceptions) Try-Catch-Syntax Programmiertechniken On-Error-Syntax Fehlersuche (Debugging) Grundlagen Fehlersuche in der Entwicklungsumgebung Debugging-Anweisungen im Code Fehlersuche in Windows.Forms-Programmen
472 472 479 483 488 490 490 492 497 500
12
Spezialthemen
12.1 12.2 12.2.1 12.2.2 12.3
Ein- und Ausgabeumleitung (Konsolenanwendungen) Systeminformationen ermitteln System.Environment-Klasse System.Management-Bibliothek (WMI) Sicherheit
505 506 507 508 511 514
Inhaltsverzeichnis
12.4 12.5 12.5.1 12.5.2 12.5.3 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.7.3
Externe Programme starten Externe Programme steuern (Automation) Grundlagen Beispiel – Daten aus einer Excel-Datei lesen Beispiel – RichTextBox mit Word ausdrucken Multithreading Grundlagen Programmiertechniken Synchronisierung von Threads API-Funktionen verwenden (Declare) Grundlagen API-Viewer Beispiele
IV
WINDOWS-PROGRAMMIERUNG
13
Windows.Forms – Einführung
13.1 13.1.1 13.1.2 13.2 13.3
Einführung Kleines Glossar Einführungsbeispiel Elementare Programmiertechniken Windows.Forms-Klassenhierarchie
14
Steuerelemente
14.1 14.1.1 14.1.2 14.1.3 14.1.4 14.2 14.2.1 14.2.2 14.2.3 14.2.4 14.2.5 14.3 14.3.1 14.3.2 14.3.3 14.4 14.4.1
Einführung Steuerelemente – Überblick Microsoft.VisualBasic.Compatibility.VB6-Steuerelemente ActiveX-Steuerelemente (alias COM-Steuerelemente) Tipps zur Verwendung der Toolbox Gemeinsame Eigenschaften, Methoden und Ereignisse Aussehen Größe, Position, Layout Eingabefokus, Validierung Sonstiges Syntaxzusammenfassung Buttons Gewöhnliche Buttons Auswahlkästchen (CheckBox) Optionsfelder (RadioButton) Textfelder Label
13
516 519 519 524 526 528 528 530 539 546 546 552 554
557 559 560 560 561 569 573
575 576 576 578 578 579 581 581 584 589 591 593 596 596 598 599 600 600
14
14.4.2 14.4.3 14.4.4 14.5 14.5.1 14.5.2 14.6 14.6.1 14.6.2 14.6.3 14.6.4 14.6.5 14.6.6 14.6.7 14.6.8 14.7 14.7.1 14.7.2 14.8 14.8.1 14.8.2 14.8.3 14.8.4 14.8.5 14.9 14.9.1 14.9.2 14.9.3 14.10 14.10.1 14.10.2 14.10.3 14.10.4 14.10.5 14.10.6 14.11 14.11.1 14.11.2 14.11.3 14.12 14.12.1 14.12.2 14.12.3
Inhaltsverzeichnis
LinkLabel TextBox RichTextBox Grafikfelder PictureBox ImageList Listenfelder ListBox CheckedListBox ComboBox ListView ListView-Beispielprogramm TreeView TreeView-Beispielprogramm DataGrid Datums- und Zeiteingabe MonthCalender DateTimePicker Schiebe- und Zustandsbalken, Drehfelder HScrollBar, VScrollBar TrackBar ProgressBar NumericUpDown DomainUpDown Gruppierung von Steuerelementen GroupBox Panel TabControl (Dialogblätter) Spezielle Steuerelemente Splitter Timer ToolTip HelpProvider ErrorProvider NotifyIcon Programmiertechniken Schleife über alle Steuerelemente Steuerelemente dynamisch einfügen Steuerelementfelder Neue Steuerelemente programmieren Vererbte Steuerelemente Steuerelemente zusammensetzen UserControl-Beispiel
601 603 608 610 610 611 612 612 624 625 628 642 649 656 660 662 662 663 665 665 666 667 668 669 669 670 670 672 674 674 678 679 682 686 688 690 690 691 694 696 697 700 701
Inhaltsverzeichnis
15
Gestaltung von Benutzeroberflächen
15.1 15.1.1 15.1.2 15.1.3 15.1.4 15.2 15.2.1 15.2.2 15.2.3 15.2.4 15.2.5 15.2.6 15.2.7 15.2.8 15.2.9 15.2.10 15.3 15.3.1 15.3.2 15.4 15.4.1 15.4.2 15.5 15.5.1 15.5.2 15.6 15.6.1 15.6.2 15.6.3 15.6.4 15.6.5 15.7 15.8 15.9 15.10 15.11 15.12 15.12.1 15.12.2 15.12.3 15.12.4
Formularspezifische Eigenschaften, Methoden und Ereignisse Aussehen Position und Größe Verwaltung von Formularen und Steuerelementen Ereignisreihenfolge Formularinterna Nachrichtenschleife (message loop, DoEvents) Windows Form Designer Code Ereignisprozeduren Formular dynamisch erzeugen Vererbung bei Formularen Windows-XP-Optik Multithreading Betriebssysteminformationen ermitteln Bildschirmauflösung (DPI) Lokalisierung von Windows-Anwendungen Verwaltung mehrerer Fenster Fenster als modale Dialoge anzeigen Mehrere gleichberechtigte Fenster öffnen MDI-Anwendungen Programmiertechniken MDI-Fenster andocken Standarddialoge Einfache Dialogboxen (MessageBox, MsgBox) Standarddialoge (OpenFileDialog, ColorDialog) Menüs Der Menüeditor Anwendung und Programmierung Menüs bei MDI-Anwendungen Kontextmenüs Menüeinträge selbst zeichnen (owner-drawn menu) Symbolleiste (ToolBar) Statusleiste (StatusBar) Tastatur Maus Zwischenablage Drag&Drop Programmiertechniken Beispiel – Symbolleiste verschieben Beispiel – Datei-Drop aus dem Windows-Explorer Drag&Drop zwischen Listenfeldern
15
709 710 710 712 713 715 718 720 722 726 727 729 730 733 745 747 751 757 758 761 768 768 772 777 777 779 780 780 782 785 786 789 795 801 804 809 814 817 818 821 823 825
16
Inhaltsverzeichnis
16
Grafikprogrammierung (GDI+)
16.1 16.1.1 16.1.2 16.1.3 16.2 16.2.1 16.2.2 16.2.3 16.2.4 16.2.5 16.2.6 16.3 16.3.1 16.3.2 16.3.3 16.3.4 16.3.5 16.3.6 16.3.7 16.4 16.4.1 16.4.2 16.4.3 16.4.4 16.4.5 16.4.6 16.4.7 16.5 16.5.1 16.5.2 16.5.3 16.5.4 16.5.5 16.5.6 16.5.7 16.5.8 16.5.9 16.5.10 16.5.11
Einführung Ein erstes Beispiel Grafik-Container (Form, PictureBox) Dispose für Grafikobjekte Elementare Grafikoperationen Linien, Rechtecke, Vielecke, Ellipsen, Kurven (Graphics-Klasse) Farben (Color-Klasse) Linienformen (Pen-Klasse) Füllmuster (Brush-Klassen) Koordinatensysteme und -transformationen Syntaxzusammenfassung Text ausgeben (Font-Klassen) Einführung TrueType-, OpenType- und Type-1-Schriftformate Schriftarten und -familien Schriftgröße Schriftattribute, Textformatierung Font-Auswahldialog Syntaxzusammenfassung Bitmaps, Icons und Metafiles Graphics- versus Image- versus Bitmap-Klasse Bitmaps in Formularen darstellen Bitmaps manipulieren Durchsichtige Bitmaps Icons Metafile-Dateien Syntaxzusammenfassung Interna und spezielle Programmiertechniken Zeichen- und Textqualität Grafikobjekte zusammensetzen (GraphicsPath) Umgang mit Regionen und Clipping Interna zu den Paint- und Resize-Ereignissen Rechteck-Auswahl mit der Maus (Rubberbox) Bitmap-Grafik zwischenspeichern (AutoRedraw) Flimmerfreie Grafik (Double-Buffer-Technik) Scrollbare Grafik Einfache Animationseffekte Bitmap mit Fensterinhalt erzeugen und ausdrucken Bitmap über Byte-Feld adressieren
831 832 833 835 836 840 840 850 853 856 859 863 867 867 868 868 871 880 890 892 894 894 896 900 907 910 912 914 915 915 918 919 922 929 932 944 946 948 951 954
Inhaltsverzeichnis
17
Drucken
17.1 17.2 17.2.1 17.2.2 17.2.3 17.2.4 17.2.5 17.3 17.4 17.4.1 17.4.2 17.4.3 17.4.4 17.5 17.5.1 17.5.2 17.5.3
Überblick Grundlagen PrintDocument-Steuerelement PrintDialog- und PageSetupDialog-Steuerelement PrintPreviewDialog-Steuerelement Druckereigenschaften und Seitenlayout Syntaxzusammenfassung Beispiel – Mehrseitiger Druck Beispiel – Inhalt eines Textfelds ausdrucken Codegerüst PrintDocument-Ereignisprozeduren Hilfsfunktionen Verbesserungsideen Fortgeschrittene Programmiertechniken Drucken ohne Steuerelemente Eigene Seitenvorschau Beispielprogramm
18
Weitergabe von Windows-Programmen (Setup.exe)
18.1 18.2 18.3 18.3.1 18.3.2 18.4 18.4.1 18.4.2 18.4.3 18.4.4 18.4.5 18.4.6
Einführung Installationsprogramm erstellen (Entwicklersicht) Installation ausführen (Kundensicht) Installation eines .NET-Programms Installation des .NET-Frameworks Installationsprogramm für Spezialaufgaben Grundeinstellungen eines Setup-Projekts Startmenü, Desktop-Icons Benutzeroberfläche des Installationsprogramms Start- und Weitergabebedingungen Dateityp registrieren Einträge in der Registrierdatenbank durchführen
ANHANG A B C D E
Abkürzungsverzeichnis Glossar Dateikennungen Inhalt der CD-ROM Quellenverzeichnis Stichwortverzeichnis
17
959 960 961 961 965 969 970 974 977 981 982 983 987 991 992 992 996 997
1003 1004 1005 1008 1008 1010 1011 1012 1014 1015 1017 1020 1021
1023 1025 1029 1035 1037 1039 1041
Vorwort Wenn Sie gerade beginnen, sich mit der neuen Welt von .NET im Allgemeinen und mit VB.NET im Besonderen auseinanderzusetzen, dann geht es Ihnen wahrscheinlich so ähnlich wie mir vor einem Dreivierteljahr: Es gibt überwältigend viele Informationen, die Zahl der neuen Paradigmen, Konzepte, Klassen scheint unendlich zu sein und wo immer man sich einliest, stellen sich mehr neue Fragen, als bestehende beantwortet werden.
Was ist .NET? Ich stelle Ihnen zu der Frage, was .NET nun wirklich ist, drei mögliche Antworten zur Auswahl: •
Einfach ein neues Kürzel der Microsoft-Marketing-Abteilung ohne konkreten Inhalt.
•
Ein neuer Versuch von Microsoft, das Internet auch serverseitig zu erobern. (Clientseitig ist das mit dem Internet Explorer schon gelungen. Aber nach wie vor laufen mehr als doppelt so viele Websites auf Unix-/Linux-Systemen als auf Windows-Systemen.)
•
Eine neue, objektorientierte Programmierschnittstelle zu fast allen Betriebssystemfunktionen.
Alle drei Antworten sind teilweise richtig, aber am ehesten trifft meiner Meinung nach der letzte Punkt zu: Der Kern von .NET ist eine neue Klassenbibliothek, die einen komfortablen, konsistenten Zugang zu nahezu allen Betriebssystemfunktionen ermöglicht. (Natürlich will die .NET-Initiative noch viel mehr. Microsoft hat ganze Bücher veröffentlicht, um zu erklären, was es mit .NET beabsichtigt, worin sich .NET von früheren Technologien unterscheidet etc. Ich möchte hier aber nicht die Versprechungen der Marketing-Abteilung wiedergeben – die finden Sie auch unter http://www.microsoft.com.)
Wozu VB.NET? Die .NET-Bibliotheken nützen Konzepte der objektorientierten Programmierung, die in dieser Form weder in Visual Basic 6 noch in C++ zur Verfügung standen. Damit die .NETBibliotheken überhaupt eingesetzt werden können, hat Microsoft gleich zwei neue Programmiersprachen geschaffen: Während sich C# primär an C-, C++- und Java-Programmierer richtet, ist VB.NET für die zahlreichen Visual-Basic-Programmierer gedacht, die die neuen .NET-Funktionen nutzen möchten. Die beiden Sprachen sind fast gleichwertig und unterscheiden sich primär durch ihre Syntax. VB.NET gewinnt seine Daseinsberechtigung also zuerst einmal dadurch, dass es die .NETBibliotheken für Visual-Basic-Programmierer zugänglich macht. Darüber hinaus bietet VB.NET im Vergleich zu VB6 aber auch eine Fülle neuer Merkmale, auf die viele Programmierer schon seit Jahren gewartet haben:
20
Vorwort
•
VB.NET ist jetzt eine vollwertige, objektorientierte Programmiersprache. VB.NET unterstützt echte Vererbung, Schnittstellen, Attribute, Delegates (eine neue Art von Funktionszeigern) etc.
•
VB.NET kann ohne Einschränkungen dazu verwendet werden, um Multithreading-Anwendungen zu entwickeln. (Das sind Anwendungen, bei denen mehrere Programmteile quasi parallel ausgeführt werden.)
•
Die in VB.NET zur Verfügung stehenden Komponenten und Bibliotheken sind entrümpelt worden und viel konsistenter zu verwenden als früher.
•
VB.NET ist erstmals auch zur Entwicklung von Internet-Anwendungen geeignet (ASP.NET). Das hat Microsoft natürlich schon bei früheren VB-Versionen versprochen. Die damals vorgestellten Konzepte überzeugten aber nicht und fanden nur geringe Verbreitung. Durchgesetzt hat sich stattdessen ASP, eine Art Script-Code, der inkompatibel zur VB6-Entwicklungsumgebung ist. Das neue ASP.NET verbindet die Vorteile von ASP mit denen von VB.NET. Damit können dynamische Webseiten nun direkt in der VB.NET-Entwicklungsumgebung programmiert werden.
VB.NET ist allerdings nicht der Nachfolger von VB6, d.h., es ist nicht VB7, wie es sich viele Programmierer erwartet haben. VB.NET ist vielmehr eine von Grund auf neue Programmiersprache! Wenn Sie VB6-Vorkenntnisse haben, wird Ihnen natürlich einiges vertraut vorkommen, intern ist aber wirklich alles neu: Das beginnt mit neuen Variablentypen, neuen Steuerelementen, einer neuen Art, Code zu verwalten und zu kompilieren, und endet bei einer unüberschaubaren Fülle neuer Bibliotheken und einer neuen Entwicklungsumgebung. Was sich in den vergangenen zehn Jahren und sechs Versionen an VB-Wildwuchs angesammelt hat, wurde neu geordnet, zum Teil durch bessere Konzepte ersetzt, zum Teil aber auch einfach gestrichen. Das Ergebnis ist eine ohne jede Einschränkung gut durchdachte Programmierumgebung, die zur Entwicklung von Anwendungen fast jeden Typs geeignet ist. Daraus folgt leider auch: VB.NET ist inkompatibel zu VB6. Vorhandener Code kann weder weiterentwickelt noch gewartet noch (mit vertretbarem Aufwand) migriert werden. Damit hat sich VB6 – eine der populärsten Programmiersprachen, die es je unter Windows gab – als Sackgasse herausgestellt. Microsoft hat hier eine ebenso unpopuläre wie mutige Entscheidung getroffen: die Kompatibilität wurde zugunsten neuer und durchwegs besserer Konzepte geopfert. (So viel Mut kann man sich freilich nur leisten, wenn man eine marktbeherrschende Position innehat ...)
Ein Blick in die Zukunft Als ich das erste Mal von den .NET-Plänen Microsofts gehört habe, war ich ziemlich skeptisch, und mit dieser Skepsis habe ich im Herbst 2001 auch mit der Arbeit an diesem Buch begonnen. Dabei wurde ich in fast jeder Hinsicht positiv überrascht: VB.NET läuft sehr stabil (obwohl es ein Version-1-Produkt ist), viele Konzepte wirken gut durchdacht, die Bibliotheken und Komponenten bieten viel mehr Möglichkeiten als bisher und sind zugleich konsistent in ihrer Anwendung. Die mitgelieferte Online-Dokumentation ist viel-
Vorwort
21
leicht nicht perfekt, aber immerhin sehr umfassend und besser als vieles, was ich für andere Programmiersprachen in letzter Zeit gelesen habe. Kurz und gut, mit .NET ist Microsoft tatsächlich ein großer Wurf gelungen! Ich bin überzeugt davon, dass die Zukunft der Windows-Programmierung .NET heißen wird. Nicht jeder wird sofort umsteigen, aber in wenigen Jahren wird die überwiegende Mehrheit aller Windows-Entwickler sicherlich .NET einsetzen. VB.NET ist natürlich nicht die einzige Programmiersprache, um .NET zu nutzen, aber es ist meiner Ansicht nach die geeignetste und am leichtesten zu erlernende.
Dieses Buch Dieses Buch versucht, einen umfassenden und detaillierten Einstieg in die VB.NET-Programmierung zu geben. Im Vordergrund stehen dabei weniger enzyklopädische Aufzählungen (die finden Sie ohnedies in der Online-Hilfe) von Klassen oder Methoden, sondern der Blick fürs Ganze, die Herstellung von Zusammenhängen und die Präsentation praxisnaher Lösungsansätze. Die zentrale Fragestellung lautet also nicht, welche Funktionen es gibt, sondern wie alltägliche Probleme aus der Programmierpraxis gelöst werden können. Inhaltlich ist das Buch in vier Teile gegliedert: •
Die einleitenden Kapitel dienen als Schnelleinstieg und geben eine Referenz der wichtigsten Änderungen für VB6-Anwender.
•
Der Grundlagenteil führt in die Syntax von VB.NET, in die objektorientierte Programmierung und – damit verbunden – in die Konzepte der Objektverwaltung unter .NET ein.
•
Die folgenden Kapitel stellen Programmiertechniken vor, die für jede Art von VB.NETAnwendung benötigt werden: Methoden zur Konvertierung und Formatierung von Basisdatentypen, Methoden zum Zugriff auf Dateien, Verfahren zur Absicherung von Programmcode, Klassen zur Verwaltung von Aufzählungen etc. Auch fortgeschrittene Techniken wie der (manchmal noch immer erforderliche) Aufruf von API-Funktionen oder Multithreading kommen nicht zu kurz.
•
Windows-Programmierung lautet schließlich die Devise im vierten Teil, der immerhin fast das halbe Buch einnimmt: Die behandelten Themen reichen von den Windows.Forms-Grundlagen über die Vorstellung der wichtigsten Steuerelemente bis hin zur Grafikprogrammierung und dem Ausdruck von Dokumenten. Damit Sie Ihre Programme problemlos weitergeben können, werden auch die neuen Möglichkeiten von SetupProjekten vorgestellt.
So wie Microsoft mit VB.NET eine Programmiersprache geschaffen hat, die sich an professionelle Anwender richtet, so orientiert sich auch diese Buch an dieser Zielgruppe. Ich setze voraus, dass Sie bereits programmieren können. Sie brauchen keine Vorkenntnisse in VB6 oder einem anderen VB-Dialekt, aber Sie sollten zumindest wissen, was eine Variable, eine Schleife und eine Prozedur ist. (Wenn das nicht der Fall ist, sollten Sie für die ersten Schritte zu einem anderen Buch greifen. Nach ein paar Wochen werden Sie den Bedarf
22
Vorwort
nach Informationen mit mehr Tiefgang verspüren – dann ist der richtige Zeitpunkt für dieses Buch gekommen.)
Viel Erfolg! Ich wünsche Ihnen, dass Ihnen mit diesem Buch der VB.NET-Einstieg gelingt, dass Sie die ersten Hürden rasch überwinden und danach wie ich Spaß an den vielen neuen Möglichkeiten gewinnen, die Ihnen VB.NET bietet!
Michael Kofler, Juni 2002 http://www.kofler.cc
PS: Manche Leser kennen sicher auch mein Buch über VB6 (Programmiertechniken, Datenbanken, Internet): Damals habe ich versucht, einen umfassenden Einstieg in die gesamte Breite von VB6 zu geben. Bei VB.NET hat sich das aber als unmöglich herausgestellt. Deswegen werden die Themen Datenbanken, Internet und XML nicht in diesem Buch, sondern in einem eigenen Band behandelt. Genauere Informationen über Inhalt und Erscheinungstermin finden Sie auf meiner Website. Der Vorteil dieser Zweiteilung besteht darin, dass ich trotz der viel größeren Themenfülle meinem Schreibstil treu bleiben kann und die einzelnen Themen so detailliert beschreiben kann, wie es mir notwendig erscheint.
Formales
TIPP
Dieses Buch ist zwar in deutscher Sprache verfasst, es enthält aber eine Menge englischer Begriffe. Das hat damit zu tun, dass die Originaldokumentation zu VB.NET natürlich englisch ist und dass ich die krampfhafte Übersetzung von Fachausdrücken eher vermeide. (Die Online-Hilfe zu VB.NET folgt da einem anderen Ansatz; dort ist wirklich fast alles eingedeutsch. Ich bin aber der Ansicht, dass die Klarheit und Eindeutigkeit des Texts darunter leidet.) Als zusätzliche Hilfe für Querleser befindet sich im Anhang ein Abkürzungsverzeichnis und ein Glossar!
Voraussetzungen/Versionen Dieses Buch geht davon aus, dass Sie zumindest VB.NET Standard besitzen. Diese Entwicklungsumgebung ist ein Produkt von Microsoft, das käuflich erworben werden muss. (Die Entwicklungsumgebung befindet sich also nicht auf der beiliegenden CD-ROM! Informationen über die unterschiedlichen Versionen der VB.NET- und VS.NET-Entwicklungsumgebungen finden Sie in Abschnitt 2.7.) Alle Beispielprogramme für dieses Buch wurden unter der folgenden Umgebung getestet: Windows 2000 deutsch mit Service Pack 2 VS.NET Enterprise Architect deutsch (final version) mit Service Pack 1
Beispielprogramme, Beispielcode Ziel der Beispielprogramme ist es nicht, fertige Anwendungen zu präsentieren, sondern bestimmte im Buch erklärte Programmiertechniken zu veranschaulichen. Aus diesem Grund gibt es in diesem Buch nicht wenige lange, sondern viele, überwiegend sehr kurze Beispiele. Aus Platzgründen sind bei Programmlistings generell nur die für den jeweiligen Abschnitt interessanten Passagen abgedruckt. Den vollständigen Code finden Sie auf der beiliegenden CD. Um Ihnen bei der Suche nach den Beispieldateien zu helfen, finden Sie am Beginn jedes Programmlistings einen Kommentar der Art 'Beispiel grafik\intro. Das bedeutet, das sich der Code im Verzeichnis grafik\intro auf der CD befindet. Im Code sind die Namen von Prozeduren durch fette Schrift hervorgehoben, um Ihnen die Orientierung zu erleichtern. Darüber hinaus fehlt bei Ereignisprozeduren von WindowsProgrammen meist die Parameterliste.
24
Formales
Statt Private Sub Button1_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles Button1.Click ... Programmcode in der Ereignisprozedur End Sub
wird nur Private Sub Button1_Click(...) Handles Button1.Click ... Programmcode in der Ereignisprozedur End Sub
abgedruckt, um die Listings kompakter und übersichtlicher zu halten. Das erscheint mir deswegen zweckmäßig, weil an Windows-Ereignisprozeduren ohnedies immer dieselben zwei Parameter übergeben werden: sender mit dem zugrunde liegenden Objekt und e mit ereignisspezifischen Daten. (Details zu Windows-Ereignisprozeduren werden in Abschnitt 13.1.2 beschrieben.)
Verweise auf die Online-Hilfe
TIPP
In diesem Buch finden Sie zahllose Verweise auf die Online-Hilfe. Diese Verweise sehen wie Webadressen aus, beginnen aber mit ms-help://. Da es möglich ist, dass sich die Adressen in der Zukunft ändern können, wird in den Verweisboxen immer auch ein Suchbegriff angegeben, der zu dem Hilfethema führt. Sie können die Hilfetexte übrigens nicht nur im Hilfebrowser von Visual Studio, sondern auch im Internet Explorer lesen. Die im Buch angegebenen Hilfeadressen beziehen sich auf die deutsche Version von Visual Studio .NET. Wenn Sie die englische Version der Online-Hilfe installiert haben, müssen Sie aus allen Hilfeverweisen die fünf Zeichen .1031 entfernen. (Diese Zeichen geben an, dass Sie die deutsche Version der Hilfe lesen möchten.) Statt durch ms-help://MS.VSCC/MS.MSDNVS.1031/vbcmn/html/vborilegacyactivexcntrlref.htm
wird der entsprechende englische Text also durch ms-help://MS.VSCC/MS.MSDNVS/vbcmn/html/vborilegacyactivexcntrlref.htm
TIPP
aufgerufen.
Sie müssen die Links übrigens nicht mühsam abtippen. Auf der beiliegenden CD befindet sich die Datei Links.html, die alle Web- und Hilfe-Links dieses Buchs enthält (geordnet nach Kapiteln). Bei den Hilfe-Links ist jeweils die deutsche und die englische Version enthalten. Wie lange die Links gültig bleiben, kann ich natürlich nicht versprechen ...
Formales
25
Verweise auf Knowledge-Base-Artikel Sie werden in diesem Buch auch Verweise auf so genannte Knowledge-Base-Artikel (kurz KB-Artikel) finden. Dabei handelt es sich um Artikel in einer Art Microsoft-Hilfedatenbank. Diese Artikel werden durch eine sechsstellige Zahl mit einem vorangestellten Q bezeichnet. Q301264 meint also den KB-Artikel 301264. Sie finden derartige Artikel auf der Microsoft-Website oder in der MSDN-Library. Die Webadresse von KB-Artikeln hat sich in der Vergangenheit immer wieder geändert. Zurzeit sieht die Adresse für den Artikel Q301264 so aus: http://support.microsoft.com/default.aspx?scid=kb;en-us;Q301264
.NET im Internet Die vielleicht beste Informationsquelle bei diffizilen Problemen der .NET-Programmierung sind die diversen Newsgruppen, die über den News-Server msnews.microsoft.com zur Verfügung gestellt werden. Einige Beispiele für derartige Gruppen sind: microsoft.public.de.german.entwickler.dotnet.vb (deutsch) microsoft.public.dotnet.languages.vb (englisch) microsoft.public.dotnet.framework.windowsforms (english)
Zum Lesen der aktuellen Beiträge in den Newsgruppen können Sie Outlook Express verwenden. Zur Suche nach alten Artikeln ist dagegen http://groups.google.com am besten geeignet. Dabei geben Sie als zusätzlichen Suchbegriff einfach dotnet ein. Wenn Sie also Probleme damit haben, eine Bitmap als GIF-Datei zu speichern, suchen Sie beispielsweise nach dotnet gif export. Darüber hinaus gibt es natürlich eine Menge Websites, die sich intensiv mit VB.NET oder .NET auseinandersetzen. Links auf derartige Seiten finden Sie auf meiner Website unter http://www.kofler.cc/vbnet.html.
Teil I
Einführung
1
Hello World
Ein erstes Beispiel (genau genommen sind es sogar zwei) soll Sie in die VB.NET-Welt einführen. Dabei geht es weniger um konkrete Programmiertechniken (für die ist in den folgenden Kapiteln noch genug Platz) als vielmehr darum, die Entwicklungsumgebung anhand konkreter Beispiele kennen zu lernen. Das Kapitel endet dann auch mit einigen Tipps zur Bedienung der Entwicklungsumgebung. Voraussetzung für dieses Kapitel ist die Installation von VB.NET bzw. VS.NET, damit Ihnen die Entwicklungsumgebung zur Verfügung steht. Wie Sie im nächsten Kapitel lernen werden, ist die VB.NET-Programmierung theoretisch auch ohne eine Entwicklungsumgebung von Microsoft möglich. Dieses Buch geht aber davon aus, dass Sie mit VB.NET bzw. VS.NET arbeiten. 1.1 1.2 1.3
Hello World (Konsolenversion) Hello World (Windows-Version) Bedienung der Entwicklungsumgebung
30 37 41
30
1 Hello World
1.1
Hello World (Konsolenversion)
Projekttyp auswählen Jedes neue Projekt beginnt damit, dass Sie die Entwicklungsumgebung starten und mit DATEI|NEU|PROJEKT ein neues Projekt starten. Es erscheint dann der in Abbildung 1.1 gezeigte Dialog, in dem Sie zwischen einer Menge Projekttypen auswählen können. Die überwiegende Mehrheit aller Projekte dieses Buchs sind Visual-Basic-Projekte, wobei die Vorlagen WINDOWS-ANWENDUNG oder KONSOLENANWENDUNG zum Einsatz kommen. Das hier vorgestellte Beispielprogramm ist eine KONSOLENANWENDUNG. Das bedeutet, dass das Programm keine eigenen Fenster verwendet, sondern alle Texteingaben und -ausgaben in einem Konsolenfenster (dem ehemaligen DOS-Fenster) durchführt.
Abbildung 1.1: Dialog zum Beginn eines neuen Projekts
Beachten Sie in Abbildung 1.1 auch die Eingabefelder NAME und SPEICHERORT: Der Speicherort gibt an, in welchem Verzeichnis das Projektverzeichnis gespeichert wird. NAME gibt den Projektnamen an. Dieser Name wird gleich mehrfach verwendet: •
für das Verzeichnis, in dem alle Projektdateien gespeichert werden;
•
für die Namen der Projektdateien (*.vbproj für das VB.NET-Projekt sowie *.sln für die Projektmappe, die auch mehrere Projekte enthalten kann);
•
für den Namen der resultierenden Programmdatei (*.exe, wird in der Entwicklungsumgebung bei den Projekteigenschaften als Assemblyname bezeichnet):
1.1 Hello World (Konsolenversion)
•
31
für den so genannten Stammnamensraum, der ebenfalls Teil der Projekteigenschaften ist. Dieser Stammnamensraum bestimmt den vollständigen Klassennamen aller Klassen, die Sie in Ihrem Projekt erstellen. Der Stammnamensraum ist vorerst unwichtig und spielt erst dann eine Rolle, wenn Sie eigene Klassenbibliotheken programmieren (siehe Kapitel 7 und speziell Abschnitt 7.8). Schon jetzt wichtig ist allerdings der Umstand, dass der Stammnamensraum nicht im Konflikt mit Klassennamen stehen sollte, die Sie in Ihrem Programm einsetzen. Beispielsweise verwendet dieses Beispielprogramm die Klasse Console, um Ausgaben durchzuführen. Sie sollten deswegen dem Beispielprogramm nicht den Namen Console geben!
Mit den Einstellungen aus Abbildung 1.1 wird das neue Projekt also im Verzeichnis D:\code\vb.net\hello-world\hello-console\ erzeugt. Die Entwicklungsumgebung besteht darauf, dass sich jedes Projekt in einem eigenen Verzeichnis befindet. Die resultierenden Dateien werden etwas weiter unten kurz beschrieben – siehe Abbildung 1.6.
Die Entwicklungsumgebung Nach der Auswahl des Projekttyps gelangen Sie in die Entwicklungsumgebung. Das Aussehen der Entwicklungsumgebung kann durch zahllose Einstellmöglichkeiten variiert werden, weswegen die Fensteranordnung nicht unbedingt so aussehen muss wie in Abbildung 1.2. Entscheidend sind vorerst nur zwei Fenster: der Projektmappen-Explorer (rechts oben) und das Codefenster (links oben). Den Projektmappen-Explorer können Sie bei Bedarf mit ANSICHT|PROJEKTMAPPENEXPLORER öffnen. Das Fenster gibt Auskunft über die zum Projekt gehörenden Dateien. Von dort können Sie durch einen Doppelklick das Codefenster öffnen. Der Code befindet sich bei Konsolenanwendungen per Default in der Datei Module1.vb. (Sie können die Datei ganz einfach im Projektmappen-Explorer umbenennen, wenn Sie möchten.)
Programmcode (Konsolenanwendungen) Bei neuen Projekten enthält das Codefenster nur ein minimales Codegerüst, das im Regelfall bereits ausreicht, um das Programm zu kompilieren. (Das Programm erfüllt dann aber natürlich keine Aufgaben.) Bei Konsolenanwendungen ist das Codegerüst besonders kurz und besteht aus nur vier Zeilen: Module Module1 Sub Main() End Sub End Module
32
1 Hello World
Abbildung 1.2: Die Entwicklungsumgebung
Die Programmausführung beginnt und endet mit Main. Main ist wiederum die einzige Prozedur des Moduls Module1. (VB.NET-Code muss sich immer in einem Modul oder in einer Klasse befinden.) Um das in Abbildung 1.5 dargestellte Hello-World-Programm zu realisieren, müssen Sie Main mit Code füllen: ' Beispiel hello-world\hello-console Option Strict On Module Module1 Sub Main() Console.WriteLine("Hello Console!") Console.WriteLine() 'leere Zeile Console.WriteLine("{0}, das heutige Datum ist: {1}!", _ Environment.UserName, Now.ToLongDateString()) Console.WriteLine() Console.WriteLine("Return drücken, um das Programm zu beenden ..") Console.ReadLine() End Sub End Module
1.1 Hello World (Konsolenversion)
33
Es ist nicht sinnvoll, hier alle Hintergründe des Codes zu beschreiben – das würde zu tief in die Welt der Klassen, Objekte, Methoden etc. führen. Einen geeigneten Einstieg in dieses Thema gibt Kapitel 6. Damit Sie aber zumindest eine grobe Vorstellung davon haben, was in dem kurzen Programm vor sich geht und warum es funktioniert, folgt nun eine kurze Beschreibung der im Code vorkommenden Schlüsselwörter. Option Strict On bewirkt, dass der Compiler eine genaue Überprüfung durchführt, ob alle Variablen deklariert sind und ob bei Zuweisungen bzw. beim Aufruf von Methoden oder Prozeduren immer die richtigen Datentypen angegeben werden. Option Strict kann viele Flüchtigkeitsfehler vermeiden helfen (siehe auch Abschnitt 4.1.4). Console ist eine Klasse, die allen Anwendungen automatisch zur Verfügung steht. Die beiden wichtigsten Methoden dieser Klasse sind Write- und ReadLine. Mit WriteLine schreiben Sie eine Zeichenkette in das Konsolenfenster. WriteLine erwartet als ersten Parameter eine Zeichenkette. Diese Zeichenkette darf die Spezialcodes {0}, {1} etc. enthalten – dann werden
an diese Stelle die weiteren optionalen Parameter eingesetzt. Das Beispielprogramm verwendet diese Art der Formatierung, um den Benutzernamen und das Datum auszugeben. (Die Angabe von Formatzeichenketten wird ausführlich in Abschnitt 8.5 beschrieben. Der von WriteLine eingesetzte Mechanismus geht auf die Methode String.Format zurück und steht bei vielen weiteren .NET-Methoden zur Verfügung.) ReadLine ist das Gegenstück zu WriteLine. Es erwartet eine mit Return abgeschlossene Eingabe, die im Konsolenfenster durchgeführt wird. ReadLine liefert die eingegebene Zeichenkette zurück. Sehr oft wird ReadLine aber wie im obigen Beispiel dazu verwendet, das Programmende so lange zu verzögern, bis der Anwender Return drückt. Die resultierende
Eingabe wird dabei gar nicht ausgewertet – es geht nur darum, zu verhindern, dass das Programm sofort nach den Konsolenausgaben endet und das Ergebnis daher nur für ein paar Sekundenbruchteile am Bildschirm zu sehen ist. Environment ist eine weitere Klasse, die allen Programmen zur Verfügung steht. Sie hilft dabei, verschiedene Informationen des Betriebssystems, des aktuellen Benutzers etc. zu ermitteln. Hier wird die Methode UserName eingesetzt, um den Namen des eingeloggten Benutzers festzustellen. Weitere Environment-Methoden werden in Abschnitt 12.2 vorgestellt. Now ist eine VB.NET-spezifische Eigenschaft, die das aktuelle Datum samt Uhrzeit liefert. Now gibt ein Objekt des Klasse Date zurück. Um ein derartiges Objekt in eine Zeichenkette der Form Mittwoch, 12. Juni 2002 umzuwandeln, stellt die Date-Klasse die Methode ToLongDateString zur Verfügung.
Kommentare: Kommentare werden mit dem Zeichen ' eingeleitet und reichen bis an das Ende der Zeile.
VERWEIS
Mehrzeilige Anweisungen: Wenn Sie eine Anweisung über mehrere Zeilen verteilen möchten (meistens deswegen, um den Code übersichtlicher anzuzeigen), müssen Sie die jeweils vorangehende Zeile mit einem Leerzeichen und dem Zeichen _ abschließen. Wie der Programmcode für Hello-Console aus objektorientierter Sicht zu verstehen ist (d.h., was ein Modul ist, warum die Codeausführung mit Main beginnt etc.) wird in Abschnitt 7.1 beschrieben.
34
1 Hello World
Codeeingabe (IntelliSense) Bei der Codeeingabe werden Sie feststellen, dass die Entwicklungsumgebung den Code automatisch einrückt (und zwar per Default um vier Zeichen pro Einrückebene, während die Listings dieses Buchs aus Platzgründen nur um zwei Zeichen pro Ebene eingerückt sind). Des Weiteren hilft Ihnen die Entwicklungsumgebung bei der Codeeingabe durch so genannte IntelliSense-Funktionen: •
Bei der Eingabe von Schlüsselwörtern werden alle für ein bestimmtes Objekt bzw. für eine Klasse zur Auswahl stehenden Eigenschaften, Methoden etc. in einer Liste anzeigt. Nach der Eingabe der ersten Zeichen reicht Tab zur Vervollständigung des Namens. Diese Funktion funktioniert meistens gut. Es gibt aber Fälle, bei denen die Funktion Schlüsselwörter nicht zu kennen glaubt, obwohl diese sehr wohl verwendet werden dürfen. Lassen Sie sich davon nicht irritieren! (Für Insider: Das Fehlverhalten betrifft meist Eigenschaften oder Methoden von vererbten Klassen bzw. von Klassen, die Schnittstellen realisieren.) Manchmal erscheint die IntelliSense-Liste nicht automatisch. In solchen Fällen lässt sich die Funktion mit Strg+Leertaste zur Zusammenarbeit bewegen.
•
Bei der Eingabe von Parametern zeigt die Entwicklungsliste in einem kleinen gelben Fenster die Parameterliste an (siehe Abbildung 1.3). Diese Funktion wäre an sich auch sehr praktisch, wenn sie ein bisschen besser funktionieren würde. Das erste Problem besteht darin, dass einzelne Methoden oft eine ganze Menge unterschiedlicher Syntaxvarianten kennen, je nachdem, wie viele Parameter übergeben werden und in welchem Typ diese Parameter übergeben werden. WriteLine kennt beispielsweise 18 verschiedene Varianten. Die IntelliSense-Funktion erkennt aber nur selten die Variante, die für Ihren Code gerade die richtige ist. In Abbildung 1.3 zeigt die Entwicklungsumgebung beispielsweise Variante 14 für einen String-Parameter an. Dabei ist offensichtlich, dass zumindest drei Parameter übergeben werden. Sie können nun die angezeigte Parameterliste einfach ignorieren (oder mit Esc ausschalten) oder mit den Cursortasten die richtige Variante suchen (siehe Abbildung 1.4). Das zweite Problem besteht darin, dass die Parameterliste oft unerwartet erscheint und Sie dabei hindert, den Cursor mit den Tasten ↑ oder ↓ in die nächste Zeile ober- bzw. unterhalb zu bewegen. Dazu müssen Sie das IntelliSense-Fenster zuerst mit Esc schließen. Sie werden bald feststellen, dass Esc zu den wichtigsten Tasten bei der Codeeingabe zählt.
Wenn Sie möchten, können Sie die IntelliSense-Funktion auch abschalten: EXTRAS|OPTIONEN, Dialogblatt TEXTEDITOR|BASIC, Optionen ANWEISUNGSABSCHLUSS.
1.1 Hello World (Konsolenversion)
35
Abbildung 1.3: Die IntelliSense-Funktion der Entwicklungsumgebung zeigt oft die falsche Parameterliste an
Abbildung 1.4: Das ist die richtige Parameterliste für diesen Aufruf von WriteLine
Bei der VB.NET-Codeeingabe spielt die Groß- und Kleinschreibung von Schlüsselwörtern und Variabeln keine Rolle. Der Editor passt Ihre Eingabe automatisch an die Schreibweise an, die bei der Definition des Schlüsselworts bzw. der Variable verwendet wurde.
Programm ausführen Um das Programm auszuführen, drücken Sie einfach auf den blauen Pfeil-Button (STARTEN) in der Standardsymbolleiste bzw. führen DEBUGGEN|STARTEN aus. Das Programm wird dazu zuerst kompiliert. Wenn dabei Fehler auftreten, werden diese angezeigt und müssen korrigiert werden. (Die meisten Fehler erkennt die Entwicklungsumgebung schon vor dem Kompilieren und kennzeichnet die betroffenen Zeilen durch eine rote gewellte Linie.)
Abbildung 1.5: Das Programm Hello Console
Wenn keine Fehler aufgetreten sind, erscheint das Programm in einem eigenen Fenster (siehe Abbildung 1.5). Noch eine Anmerkung zu dieser Abbildung: Normalerweise wird der Text in Konsolenfenstern in weißer Schrift auf schwarzem Hintergrund dargestellt. Da das weder am Bildschirm noch in einem Buch besonders augenfreundlich ist, habe ich die Farben für alle Bildschirmabbildungen geändert. Dazu klicken Sie den Fenstertitel mit der rechten Maustaste an und ändern im Eigenschaftsdialog FARBEN die entsprechenden Einstellungen.
36
1 Hello World
Projekt- und Quellcodedateien Bis jetzt kamen Sie in der Entwicklungsumgebung nur mit der Codedatei Module1.vb in Kontakt. Tatsächlich hat die Entwicklungsumgebung aber eine ganze Reihe weiterer Dateien erzeugt (siehe Abbildung 1.6), die hier kurz beschrieben werden: Module1.vb
VB.NET-Quellcode
AssemblyInfo.vb
VB.NET-Quellcode mit Informationen über die Programmversion, den Entwickler, die Firma, das Copyright etc.; die Angabe dieser Informationen ist optional
projname.vbproj
VB.NET-Projektdatei mit Informationen darüber, aus welchen Dateien das Projekt besteht, welche Einstellungen gelten, welche Verweise auf zusätzliche Bibliotheken eingerichtet wurden etc.; viele Einstellungen dieser Datei können durch den EIGENSCHAFTENDialog zum Projekt eingestellt werden
projname.vbproj.user
Ergänzung zur VB.NET-Projektdatei, enthält benutzerspezifische Einstellungen
projname.sln
VS.NET-Projektmappe mit Informationen darüber, welche Projekte zur Mappe gehören; bei einfachen Anwendungen enthält die Datei nur einen Verweis auf projname.vbproj; prinzipiell ist es aber möglich, innerhalb einer Projektmappe mehrere Projekte zu verwalten (die sogar mit unterschiedlichen Programmiersprachen ausgeführt werden können)
projname.suo
Ergänzung zu projname.sln, enthält benutzerspezifische Einstellungen
bin\*
das zur Ausführung geeignete Kompilat des Programms
obj\*
temporäre Dateien, die während des Kompilierens erzeugt werden
Neben den Quellcode- und Konfigurationsdateien erzeugt die Entwicklungsumgebung beim Kompilieren das ausführbare Programm. Zum Kompilieren wird das temporäre Verzeichnis obj\debug (für die Debug-Version) bzw. obj\release (für die Endversion des Programms, das an den Kunden weitergeben werden soll) verwendet. Das Endergebnis, d.h. die ausführbare Datei projname.exe sowie eventuell zusätzliche Debugging-Informationen zur Fehlersuchen (Datei projname.pdb), werden anschließend in das Verzeichnis bin kopiert. Dieser Vorgehensweise mutet vielleicht ein wenig kompliziert an, sie hat aber Vorteile bei komplexen Projekten, weil dann nur die Dateien neu kompiliert werden müssen, die sich geändert haben. Allerdings befinden sich immer mindestens zwei Kopien des ausführbaren Programms im Projektverzeichnis. Am besten ist es, wenn Sie das obj-Verzeichnis einfach ignorieren und nur das bin-Verzeichnis berücksichtigen!
1.2 Hello World (Windows-Version)
37
Abbildung 1.6: Die Quelldateien des Beispielprogramms
1.2
Hello World (Windows-Version)
Die Entwicklung eines Windows-Programms beginnt damit, dass Sie als Projekttyp WINDOWS-ANWENDUNG angeben. Die Entwicklungsumgebung erzeugt damit ein neues Projekt, in das es ein erstes (noch leeres) Formular samt dem dazugehörigen Verwaltungscode einfügt. In der Entwicklungsumgebung wird das leere Fenster sichtbar (Form1.vb).
Steuerelemente einfügen Der erste Schritt zu dem in Abbildung 1.8 dargestellten Beispielprogramm besteht darin, in das Fenster die erforderlichen Steuerelemente einzufügen. (Steuerelemente sind Bedienungs- bzw. Anzeigeelemente eines Windows-Programms, z.B. Buttons, Listenfelder etc.). Das Beispielprogramm benötigt nur drei Steuerelemente, zwei Label-Felder zur Darstellung kurzer Texte sowie einen Button, um das Programm zu beenden. Um Steuerelemente einzufügen, öffnen Sie das Toolbox-Fenster (ANSICHT|TOOLBOX). Die Toolbox zeigt nur dann Steuerelemente an, wenn im Arbeitsbereich der Entwicklungsumgebung ein Formular angezeigt wird. Wenn die Toolbox nach kurzer Zeit automatisch wieder ausgeblendet ist, fixieren Sie das Fenster vorübergehend durch einen Klick auf die Pin-Up-Nadel. Damit verkleinert sich zwar Ihr Arbeitsbereich, aber dafür können Sie komfortabel arbeiten, ohne ständig auftauchenden und wieder verschwindenden Fenstern nachzujagen. In der Toolbox klicken Sie zuerst das gewünschte Steuerelement an und zeichnen dann im Formular mit der Maus einen Rahmen, der die gewünschte Größe angibt. Die Entwicklungsumgebung fügt das Steuerelement an der Stelle des Rahmens ein.
38
1 Hello World
Abbildung 1.7: Die VB.NET-Benutzeroberfläche beim Entwurf eines Windows-Programms
Eigenschaften einstellen Der zweite Schritt besteht darin, die Eigenschaften des Formulars und der Steuerelemente einzustellen. Dazu klicken Sie das betreffende Objekt an. Im Eigenschaftsfenster (ANSICHT| EIGENSCHAFTSFENSTER) werden nun alle Eigenschaften dieses Objekts angezeigt, wobei von den Defaultwerten abweichende Einstellungen durch eine fette Schrift hervorgehoben sind. Beim Beispielprogramm Hello Windows müssen Sie nur die Text-Eigenschaften des Buttons sowie des Formulars einstellen (mit den Texten Ende bzw. Hello Windows). Alle anderen Voreinstellungen der Entwicklungsumgebung können so bleiben, wie sie sind.
Ereignisprozeduren einfügen Der Programmfluss von Windows-Programmen wird durch Ereignisse bestimmt. Ereignisse sind z.B. eine Mausbewegung, das Anklicken eines Buttons oder das Laden des Programms. Programmcode wird immer nur dann ausgeführt, wenn das Programm ein Ereig-
1.2 Hello World (Windows-Version)
39
nis feststellt und es eine dazu passende Ereignisprozedur gibt. (Anders als bei Konsolenanwendungen gibt es keine Main-Prozedur.) Hello Windows soll auf zwei Ereignisse reagieren: Beim Laden des Programms soll es in den
beiden Labelfeldern den Benutzernamen und das aktuelle Datum anzeigen. Beim Anklicken von ENDE soll das Programm beendet werden. Um die zwei entsprechenden Ereignisprozeduren einzufügen, führen Sie zuerst einen Doppelklick auf den Button aus. Damit fügt die Entwicklungsumgebung selbstständig eine noch leere Button1_Click-Prozedur in den Code ein und wechselt in das Codefenster. Dort geben Sie in dieser Prozedur als einzige Anweisung Close an. (Damit wird das Fenster geschlossen und das Programm beendet.) Wechseln Sie zurück in das Formularfenster und führen Sie nun einen Doppelklick im Innenbereich des Formulars aus. Die Entwicklungsumgebung fügt damit eine leere Form1_Load-Prozedur ein, in die Sie die beiden folgenden Zeilen einfügen: Label1.Text = "Benutzer: " + Environment.UserName Label2.Text = "Datum: " + Now.ToLongDateString()
HINWEIS
Damit erreichen Sie, dass beim Programmstart die beiden Label-Texte mit dem Benutzernamen und dem Datum initialisiert werden. Einzelne Objekte eines Windows-Programms kennen normalerweise mehrere Ereignisse. Load und Click waren die Defaultereignisse des Formulars bzw. des Buttons und konnten deswegen komfortabel per Doppelklick in den Code eingefügt werden. Bei allen anderen Ereignissen müssen Sie zum Einfügen im Codefenster zuerst im linken Listenfeld das Objekt (Steuerelement) auswählen und dann im rechten Listenfeld das gewünschte Ereignis.
Programmcode Der gesamte Programmcode für das Hello-Windows-Projekt besteht aus weit mehr als den beiden Ereignisprozeduren, die Sie selbst eingegeben haben. Die folgenden Zeilen zeigen die Struktur des Codes. Dabei sehen Sie, dass sich der gesamte Code innerhalb einer Klasse befindet, die das Fenster gewissermaßen beschreibt. Der größte Teil des Codes ist allerdings normalerweise gar nicht sichtbar. Es handelt sich dabei um den Block Vom Windows Form Designer generierter Code. Dieser Block kann in der Entwicklungsumgebung durch Anklicken des +-Zeichens auseinander geklappt werden, wenn Sie sich für die Details interessieren. Der Code enthält Anweisungen, um das Fenster mit seinen Steuerelementen und allen im Eigenschaftsfenster getätigten Einstellungen zu erzeugen und bei Programmende wieder zu schließen. Bei einem noch leeren WindowsProjekt ist der Codeblock ca. 35 Zeilen lang; er wächst, je mehr Steuerelemente Sie in das Fenster einfügen und je mehr Eigenschaften Sie einstellen. Im Regelfall können Sie diesen Codeblock einfach ignorieren. Er dient einfach dazu, eine Verbindung zwischen der internen Programmlogik und der nach außen hin sichtbaren Entwicklungsumgebung (dem so genannten Windows Form Designer) herzustellen. Auf
40
1 Hello World
keinen Fall sollten Sie den Code ändern, solange Sie nicht genau verstehen, wofür die einzelnen Anweisungen verantwortlich sind. ' Beispiel hello-world\ Option Strict On Public Class Form1 Inherits System.Windows.Forms.Form [ ... Vom Windows Form Designer generierter Code ... ] Private Sub Form1_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load Label1.Text = "Benutzer: " + Environment.UserName Label2.Text = "Datum: " + Now.ToLongDateString() End Sub
VERWEIS
Private Sub Button1_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles Button1.Click Close() End Sub End Class
Eine ausführlichere Einführung in die Windows-Programmierung folgt in Kapitel 13. Im Mittelpunkt des Kapitels steht ein etwas komplexeres Beispielprogramm. Eine detaillierte Beschreibung des Codeblocks Vom Windows Form Designer generierter Code finden Sie in Abschnitt 15.2.2.
Programm ausführen Wie bei einer Konsolenanwendung kann das fertige Programm mit STARTEN ausgeführt werden. Sie sehen das Ergebnis Ihrer Mühe in Abbildung 1.8. (Auf der beiliegenden CDROM finden Sie das gesamte Beispielprojekt im Verzeichnis hello-world\hello-windows.)
Abbildung 1.8: Das Programm Hello Windows
1.3 Bedienung der Entwicklungsumgebung
1.3
41
Bedienung der Entwicklungsumgebung
Der Platz reicht nicht aus, um hier alle Bedienungsdetails der VB.NET- bzw. VS.NET-Entwicklungsumgebung zu beschreiben. Und selbst wenn es mehr Platz gäbe, könnte dieser sicherlich besser genutzt werden, statt auf vielen Seiten die Menüs, Symbolleisten und Fenster der Entwicklungsumgebung aufzuzählen: Im Großen und Ganzen ist die Entwicklungsumgebung intuitiv zu bedienen, und Sie werden nach wenigen Tagen auch ohne langatmige Erklärungen damit zurecht kommen.
VERWEIS
Daher beschränkt sich dieser Abschnitt auf die Konfigurationsmöglichkeiten sowie auf einige Aspekte, die nicht sofort ins Auge springen bzw. bei denen es wesentliche Unterschiede im Vergleich zu VB6 gibt. Einzelne Aspekte der Entwicklungsumgebung werden in den Kapiteln behandelt, in die sie thematisch passen, der Objektbrowser also beispielsweise in Kapitel 6, das den Umgang mit .NET-Bibliotheken beschreibt. Objektbrowser: Abschnitt 6.3 Debugging-Elemente (Überwachungsfenster, Threadfenster etc.): Abschnitt 11.2 Fenster zur Bearbeitung von Setup-Projekten: Kapitel 18
1.3.1
Layout der Entwicklungsumgebung
Die Entwicklungsumgebung setzt sich aus Dutzenden von Fenstern zusammen, von denen die meisten an einem der vier Ränder des Hauptfensters angedockt sind. Die eigentlichen Dokumentenfenster (Programmcode, Formulare etc.) werden in dem verbleibenden Innenbereich angezeigt (siehe z.B. Abbildung 1.7). Das ermöglicht nur dann ein komfortables Arbeiten, wenn Sie einen ausreichend großen Monitor besitzen. Wenn Sie mit dem Standardlayout nicht zufrieden sind, können Sie die Position, Größe und das Docking-Verhalten fast aller Fenster selbst konfigurieren. Meine Experimente endeten allerdings immer wieder damit, dass ich reumütig zum Defaultlayout zurückgekehrt bin (EXTRAS|OPTIONEN|UMGEBUNG|FENSTERLAYOUT ZURÜCKSETZEN). So groß die Konfigurationsvielfalt in der Theorie ist, so vielfältig sind leider auch die Probleme in der Praxis, sobald man vom Defaultlayout abweicht. Besonders oft hatte ich das Problem, dass sich einzelne Fenster nicht mehr öffnen ließen (z.B. das EIGENSCHAFTS-Fenster oder das THREADING-Fenster zur Fehlersuche). Auch der Versuch, die Dokumentenfenster als MDI-Fenster anzuzeigen (EXTRAS|OPTIONEN|UMGEBUNG|MDI-UMGEBUNG), ist gescheitert: Die ganze Entwicklungsumgebung ist darauf nicht ausgerichtet, merkt sich keine Fenstergrößen, platziert mehr oder weniger willkürlich andere Fenster in den Vordergrund etc. Fazit: Man kann mit der Entwicklungsumgebung in der Defaultumstellung gut arbeiten. Die Umgebung ist aber (noch) zu wenig ausgereift, um auch andere Layouts ordentlich zu
42
1 Hello World
unterstützen. Sie sparen Zeit und Nerven, wenn Sie das Layout so lassen, wie es ist, und sich mit den kleinen Unzulänglichkeiten abfinden.
1.3.2
Menüs und Symbolleisten
Wie ein Menü zu bedienen ist, brauche ich an dieser Stelle sicher nicht zu erklären. Ebenso ist eine Beschreibung der zahlreichen Menükommandos hier nicht sinnvoll. Dennoch erscheint es angebracht, hier einige nicht ganz selbstverständliche Hinweise zusammenzufassen: •
Das Menü ist kontextabhängig! Viele Menükommandos stehen nur dann zur Verfügung, wenn gerade ein bestimmtes Fenster oder ein bestimmter Fensterbereich der Entwicklungsumgebung gerade aktiv ist. Zum Teil muss in diesem Fenster(bereich) sogar noch ein ganz bestimmtes Element aktiviert sein! Ein besonders frappierendes Beispiel ist das häufig benötigte Kommando PROJEKT|EIGENSCHAFTEN, mit dem globale Eigenschaften des Projekts verändert werden können. Dieses Kommando ist nur zugänglich, wenn das Fenster PROJEKTMAPPEN-EXPLORER aktiv ist und darin ein Projekt ausgewählt ist. Zum Teil sind diese kontextabhängigen Menüeinträge ganz praktisch, weil Sie vermeiden, dass das Menü noch unübersichtlicher ist als dies ohnedies schon der Fall ist. Manchmal wirkt die Entwicklungsumgebung aber auch schlicht unlogisch. Fazit: Senden Sie mir bitte keine E-Mails der Form: Lieber Herr Kofler, auf Seite 345 schreiben Sie, man könne mit Projekt|Eigenschaften die Default-Importe verändern. Bei mir gibt es diesen Menüeintrag aber nicht!
•
Auch die Symbolleisten und andere Elemente der Entwicklungsumgebung sind kontextabhängig: Was für das Menü gilt, gilt auch für andere Elemente der Entwicklungsumgebung. Symbolleisten tauchen auf und verschwinden, je nachdem, was Sie gerade tun bzw. welches Fenster aktiv ist. In der Toolbox sind nur dann Steuerelemente zu finden, wenn gerade ein Formularfenster geöffnet ist. Die Debugging-Fenster stehen erst zur Verfügung, wenn Sie tatsächlich ein Programm ausführen etc.
•
Menüs und Symbolleisten können verändert werden: Mit EXTRAS|ANPASSEN|SYMBOLLEISTE können Sie das Hauptmenü und die Symbolleisten ein- und ausschalten. (Das Hauptmenü gilt intern ebenfalls als Symbolleiste, kann aber nicht ausgeschaltet werden.) Solange der Dialog aktiv ist, können Sie Menükommandos und Buttons zwischen den Symbolleisten verschieben bzw. kopieren (Strg-Taste). Eine schier endlose Liste aller zur Verfügung stehenden Kommandos finden Sie im Dialogblatt BEFEHLE. (Übrigens ist die Vorgehensweise bei der Veränderung von Menüs und Symbolleisten exakt dieselbe wie beim Office-Paket.)
1.3 Bedienung der Entwicklungsumgebung
1.3.3
43
Tastenkürzel
Die Benutzeroberfläche kann durch unzählige Tastenkürzel sehr effizient bedient werden – wenn man die Kürzel einmal erlernt hat. Bevor Sie sich damit auseinander setzen, sollten Sie sich zuerst für eine der möglichen Defaultbelegungen entscheiden. Zur Auswahl stehen mehrere Schemas: Standardeinstellung, Visual Basic 6, VS6, VC++2 oder VC++6. Das Schema kann beim ersten Start der Entwicklungsumgebung sowie im Dialogblatt EXTRAS|OPTIONEN|UMGEBUNG|TASTATUR ausgewählt werden. Darüber hinaus kann jedem der zahllosen Kommandos der Benutzeroberfläche ein eigenes Tastenkürzel zugewiesen werden. Die so veränderte Tastaturbelegung wird dann in einem eigenen Tastaturschema gespeichert. Alle Einstellungen erfolgen im gerade erwähnten Dialogblatt (siehe Abbildung 1.9).
Abbildung 1.9: Tastaturkonfiguration für die Entwicklungsumgebung
1.3.4
Online-Hilfe
In die Online-Hilfe gelangen Sie am einfachsten mit F1. Nach Möglichkeit führt F1 kontextabhängig zum richtigen Thema. Insbesondere sollte F1 zur Beschreibung der Klasse, Methode, Eigenschaft etc. führen, an der sich der Cursor im Codefenster gerade befindet. Wenn das nicht klappt (was manchmal vorkommt), sollten Sie in den Objektbrowser wechseln (ANSICHT|ANDERE FENSTER), dort das Schlüsselwort suchen und die Hilfe vom Objektbrowser aus öffnen.
TIPP
44
1 Hello World
Falls Sie die Visual-Basic-Tastenkürzel verwenden, gelangen Sie mit Shift+F2 besonders rasch in den Objektbrowser bzw. zur Definition eines Schlüsselworts im Code. Diese Tastenkombination sucht im Objektbrowser das Schlüsselwort, über dem sich der Cursor gerade befindet. Esc führt zurück ins Codefenster.
Neben diesem herkömmlichen Hilfesystem bietet die Entwicklungsumgebung auch ein dynamisches Hilfesystem. Wenn Sie dieses Hilfefenster anzeigen (HILFE|DYNAMISCHE HILFE), versucht die Entwicklungsumgebung passend zur aktuellen Arbeit die richtigen Hilfethemen anzuzeigen. Während der ersten ein bis zwei Wochen mit VB.NET ist das ganz praktisch, danach beginnt es zu nerven und der Neuigkeitswert sinkt gegen null. Der Umfang der Hilfe ist wirklich phantastisch. Es gibt fast kein Thema, das dort nicht behandelt wird. Allerdings ist es manchmal recht schwierig, zur richtigen Seite zu gelangen. Machen Sie sich also mit den Suchmöglichkeiten vertraut – es lohnt sich. Meist sieht der Suchvorgang bei mir so aus:
TIPP
• Die Suche beginnt mit ANSICHT|NAVIGATION|SUCHEN, wo ich ein bis drei möglichst konkrete Stichwörter eingebe. • Dann sortiere ich die Suchergebnisse nach dem SPEICHERORT, um auf diese Weise möglichst rasch die interessanten von den uninteressanten Ergebnissen zu trennen. • Wenn ich einen einigermaßen hilfreichen Eintrag gefunden habe, verwende ich den Button INHALT SYNCHRONISIEREN (ein blauer Doppelpfeil) in der Symbolleiste des Hilfefensters. Damit sehe ich, wo in der verzweigen Hierarchie der Hilfethemen ich mich befinde. Oft sind auch die benachbarten Seiten hilfreich.
1.3.5
Codeeingabe
Codeeinrückung
TIPP
Die Einrückung von Code erfolgt in der Regel automatisch. (Die einzige Ausnahme sind mehrzeilige Anweisungen, deren Zeilenenden durch das Zeichen _ gekennzeichnet sind.) Die Parameter für die automatische Einrückung können mit EXTRAS|OPTIONEN|TEXTEDITOR|BASIC|TABSTOPPS eingestellt werden. (Beispielsweise verwendet der gesamte in diesem Buch abgedruckte Code eine Einrücktiefe von nur zwei Zeichen statt der Defaulteinstellung von vier Zeichen.) Wenn der Editor bei Änderungen der Codestruktur mit der automatischen Einrückung durcheinander kommt, markieren Sie einfach die ganze Codepassage und drücken Tab. Damit wird der gesamte Bereich neu (und korrekt) eingerückt.
1.3 Bedienung der Entwicklungsumgebung
45
#-Kommandos (Direktiven) #-Kommandos beginnen mit dem Zeichen #. Es handelt sich dabei nicht um VB.NETKommandos, sondern um Kommandos, die vom Compiler bzw. vom Editor ausgewertet werden. Sie dienen beispielsweise dazu, manche Codeteile je nach Abhängigkeit einer Konstante zu berücksichtigen oder Codeteile zu einer Gruppe zusammenzufassen, die vollständig zusammengeklappt werden kann. #-Kommandos #Region "name" ... code #End Region
gruppiert Code zu einer Region, die zusammengeklappt werden kann. So definierte Regionen werden beim Laden eines Projekts automatisch zusammengeklappt. #Region wird beispielsweise dazu verwendet, den
automatisch erzeugten Code von Windows-Formularen auszublenden (Vom Windows Form Designer generierter Code). Mehr Informationen zu diesem automatisch erzeugten Code finden Sie in Abschnitt 15.2.2. #Const constname = "xy"
definiert eine Konstante. Die Konstante ist nur für andere #-Kommandos sichtbar und gilt nur für die jeweilige Codedatei.
#If ausdruck1 Then ... code1 #ElseIf ausdruck2 Then ... code2 #Else ... code3 #End If
definiert eine Verzweigung. Der Compiler berücksichtigt nur den Codeabschnitt, dessen zugeordneter Ausdruck zutrifft. In den Ausdrücken können selbst definierte Konstanten sowie die drei vordefinierten Konstanten Config, Debug und Trace ausgewertet werden.
#ExternalSource(filename, n) [error] #End External Source
stellt eine Verbindung zu einer externen Datei her (z.B. bei *.aspx-Dateien). Das Kommando ist nur für den internen Einsatz durch die Entwicklungsumgebung gedacht. n gibt eine Zeilennummer in der externen Datei an. error kann optional angeben, in welcher Zeile der externen Datei ein Fehler aufgetreten ist.
1.3.6
Debug und Trace (jeweils True/False) sowie alle weiteren
Konstanten können im Eigenschaftsdialog des Projekts eingestellt werden.
Befehlsfenster
Das Befehlsfenster kann über das Menü ANSICHT|ANDERE FENSTER geöffnet werden. Während der Fehlersuche (d.h., während ein Programm unterbrochen ist) können in diesem Fenster einfache Kommandos ausgeführt werden. Insbesondere können Sie mit ?name den Inhalt einer Variablen darstellen, mit name=123 den Wert einer Variablen ändern etc. Allerdings bietet das Befehlsfenster bei weitem nicht so viele Möglichkeiten wie das aus VB6 vertraute Direktfenster. Insbesondere kann bei ? immer nur ein Ausdruck angegeben wer-
46
1 Hello World
den. Des weiteren ist es nicht möglich, komplexe Kommandos auszuführen (etwa eine Schleife). Das Befehlsfenster kennt zwei Modi: einen so genannten unmittelbaren Modus und den Befehlsmodus. •
Unmittelbarer Modus: Dieser Modus ist per Default aktiv. Sie erkennen diesen Modus daran, dass in der Titelleiste des Befehlsfensters der Text UNMITTELBAR angezeigt wird. (Falls gerade der Befehlsmodus aktiv ist, gelangen Sie mit dem Kommando immed zurück in den unmittelbaren Modus.) Im unmittelbaren Modus können Sie einfache VB-Kommandos ausführen, selbst definierte Funktionen oder Prozeduren aufrufen sowie mit ? den Inhalt einer Variablen ausgeben. (Die aus VB6 geläufige Schreibweise ?x,y,z zur Ausgabe mehrerer Variablen ist allerdings nicht mehr möglich.)
•
Befehlsmodus: Um in den Befehlsmodus zu wechseln, führen Sie das Kommando >cmd aus. (Das Zeichen > muss ebenfalls eingegeben werden!) Sie erkennen den Befehlsmodus daran, dass von nun an jeder Eingabezeile das Zeichen > vorangestellt wird. Im Befehlsmodus können Sie Kommandos zur Steuerung der Entwicklungsumgebung ausführen. Beispielsweise öffnet das Kommando Datei.NeueDatei den Dialog, um dem Projekt eine neue Datei hinzuzufügen. (Die Kommandos müssen in der Sprache der Entwicklungsumgebung eingegeben werden. Die zur Auswahl stehenden Kommandos finden Sie mit EXTRAS|OPTIONEN im Dialogblatt UMGEBUNG|TASTATUR.) Neben den ausgeschriebenen Kommandos gibt es einige vordefinierte Abkürzungen für besonders wichtige Kommandos. Beispielsweise setzt bp in der gerade aktuellen Zeile des Codefensters einen Haltepunkt (break point) bzw. entfernt ihn wieder. Eine Liste der zur Auswahl stehenden Abkürzungen finden Sie in der Hilfe (suchen Sie nach vordefinierte Befehls-Aliase). Sie können Befehle zur Steuerung der Entwicklungsumgebung übrigens auch im unmittelbaren Modus eingeben, wenn Sie das Zeichen > voranstellen.
TIPP
Jetzt bleibt nur noch die Frage offen, was dieser neue Befehlsmodus eigentlich bringt: Meiner Ansicht nach nicht viel. Die meisten Kommandos sind über das Menü einfacher zugänglich (und auch das Menü kann per Tastatur gut bedient werden). Der Befehlsmodus ermöglicht zwar auch die Ausführung von Kommandos, die im Menü nicht enthalten sind – aber das ist eine eher exotische Anwendung für echte Freaks. (Wer ein im Menü fehlendes Kommando häufig per Tastatur ausführen will, kann das Kommando ja ohne weiteres in das Menü einbauen: ANSICHT|SYMBOLLEISTEN|ANPASSEN.) Wenn Sie im Befehlsfenster VB-Kommandos eingeben möchten, aber jede Eingabe mit Der Befehl xy ist ungültig quittiert wird, sind Sie wahrscheinlich versehentlich in den Befehlsmodus geraten. Führen Sie zum Wechsel in den unmittelbaren Modus das Kommando immed aus!
1.3 Bedienung der Entwicklungsumgebung
1.3.7
47
Defaulteinstellungen für neue Projekte
Im Verzeichnis Programme\Microsoft Visual Studio .NET\Vb7\VBWizards gibt es für jeden Projekttyp (z.B. Windows-Anwendungen, Konsolenanwendungen etc.) ein eigenes Verzeichnis. Das Unterverzeichnis Template enthält wiederum einige Dateien mit diversen Defaulteinstellungen, die Sie verändern können (erstellen Sie vorher ein Backup!). Wenn Sie beispielsweise die Defaulteinstellungen für Windows-Anwendungen verändern möchten, werfen Sie einen Blick in das folgende Verzeichnis. (Wenn Sie mit der englischen VS.NETVersion arbeiten, lautet die Sprachnummer 1033.) Programme\Microsoft Visual Studio .NET\Vb7\VBWizards\WindowsApplication\Templates\1031
Das Verzeichnis enthält drei Dateien: •
Form.vb enthält eine Codeschablone für das Formular des neuen Programms.
•
AssemblyInfo.vb enthält einige Felder betreffend Copyright, Firmennamen etc. Wenn Sie wollen, dass bei jedem neuen Projekt Ihr Name (oder Ihr Firmenname) eingetragen wird, verändern Sie diese Felder.
•
WindowsApplication.vbproj enthält schließlich einige allgemeine Defaulteinstellungen. (Diese Einstellungen finden Sie in der Entwicklungsumgebung im Dialog PROJEKT|EIGENSCHAFTEN. Damit dieser Menüpunkt zur Auswahl steht, müssen Sie im PROJEKTMAPPEN-EXPLORER den Namen Ihres Projekts anklicken.)
Wenn Sie beispielsweise möchten, dass für alle neuen Windows-Anwendungen die Option Option Strict On gilt (Hintergrundinformationen zu dieser nützlichen Option finden Sie in Abschnitt 4.1.4), dann laden Sie die Datei WindowsApplication.vbproj in einen Texteditor und fügen die fett markierte Zeile ein. (Lassen Sie die anderen Einstellungen unverändert!)
HINWEIS
Die obige Einstellung gilt nur für Windows-Anwendungen (nicht aber für die zahlreichen anderen VB.NET-Projekttypen)! Wenn Option Strict On beispielsweise auch für Konsolenanwendungen gelten soll, müssen Sie auch deren Schablone ändern (Verzeichnis ConsoleApplication). Es scheint leider keine Möglichkeit zu geben, Option Strict global für alle Projekttypen zu aktivieren.
2
Das .NET-Universum
VB.NET basiert auf dem .NET-Framework, also den neuen .NET-Klassenbibliotheken und den dazugehörenden Werkzeugen. Viele .NET-Details und -Interna werden bei der VB.NET-Programmierung von der Entwicklungsumgebung verborgen, dennoch ist es natürlich zweckmäßig, wenn Sie zumindest eine ungefähre Vorstellung haben, was hinter den Kulissen vor sich geht. Dieses Kapitel will einen Einstieg in das .NET-Universum geben. Es beschreibt die Hintergründe, die zur Entwicklung von .NET führten, stellt einige zugrunde liegende Techniken vor, gibt einen Überblick über die zur Auswahl stehenden Entwicklungswerkzeuge etc. Bevor die weiteren Kapiteln die Details der VB.NET-Programmierung erläutern, geht es hier um den Blick aufs Ganze. 2.1 2.2 2.3 2.4 2.5 2.6 2.7
Wozu .NET? Das .NET-Framework Architektur Sicherheitsmechanismen .NET und das Internet Programmiersprachen (C# versus VB.NET) Entwicklungsumgebungen
50 53 55 61 65 67 69
50
2 Das .NET-Universum
2.1
Wozu .NET?
Vor .NET beruhte die Programmentwicklung unter Windows primär auf der für C- und C++-Programmierer konzipierten MFC (Microsoft Foundation Class Library) sowie auf dem für C++ und VB6 gedachten COM (Component Object Model). Beide Konzepte waren bei ihrer Einführung modern und stellten einen Fortschritt im Vergleich zu vorhandenen anderen Technologien dar. Im Laufe der Zeit tauchten aber eine Reihe von Problemen auf, die das Leben für Programmierer zunehmend mühsam machten: •
DLL-Hell: COM-Bibliotheken werden bei der Installation eines Programms in Form von DLL-Dateien in das Windows-Systemverzeichnis kopiert. Falls sich dort bereits ältere Versionen derselben Bibliothek befanden, wurden diese überschrieben. Allerdings waren die neuen Bibliotheken zum Teil in (winzigen) Details inkompatibel, so dass nach der Installation eines neuen Programms oft ältere Programme, die sich auf ältere Versionen einer COM-Bibliothek verließen, nicht mehr liefen. Dieses Problem wird mit dem einprägsamen Begriff DLL-Hell bezeichnet.
•
Speicherverwaltung: Bei COM ist der Programmierer dafür verantwortlich, nicht mehr benötigte Objekte explizit freizugeben. Bei einzelnen Objekten ist das nicht weiter schwierig. Wenn aber mehrere Objekte (womöglich zirkulär) aufeinander verweisen, kann es schlicht unmöglich sein, diese Objekte aus dem Speicher zu entfernen. Daher verbrauchten viele Programme aufgrund nicht freigegebener Objekte zunehmend mehr Speicher, je länger sie liefen.
•
Sicherheit: Bei herkömmlichen *.exe-Dateien gilt das Motto: Alles oder nichts. Sobald ein Programm läuft, hat es uneingeschränkten Zugriff auf alle Betriebssystemfunktionen. (Bei Windows NT/2000/XP ist der Zugriff durch die Datei- und Systemzugriffsrechte etwas limitiert, aber das hilft auch nichts, wenn ein Programm von Administrator gestartet wird). Daraus ergeben sich natürlich massive Sicherheitsprobleme.
•
Konsistenz, Objektorientierung: Das COM-Konzept ist mittlerweile etwa zehn Jahre alt, und viele Bibliotheken tragen in sich auch den Ballast der letzten zehn Jahre. Das führt dazu, dass die Bibliotheken in sich und im Vergleich mit anderen Bibliotheken vollkommen inkonsistent sind. Vergleichbare Operationen werden in unterschiedlichen Bibliotheken auf unterschiedliche Weise durchgeführt. Der Einarbeitungsaufwand für den Programmierer ist dementsprechend groß. Außerdem haben sich in den vergangenen zehn Jahren die Anforderungen an eine moderne, objektorientierte Klassenbibliothek stark gewandelt. COM-Bibliotheken entsprechen diesen Anforderungen meist nicht mehr.
Diese Probleme sind so grundlegend, dass eine Korrektur durch ein Update oder Bugfix unmöglich ist. Die Wartung des auf COM basierenden Codes stellte sich als zunehmend schwierig bis unmöglich dar. Aus diesen (und vielen anderen Gründen) hat sich Microsoft für eine Neuentwicklung der gesamten Infrastruktur für Programmentwickler entschlossen. Diese Infrastruktur umfasst neue Klassenbibliotheken, neue Mechanismen zum Austausch von Objekten zwischen verschiedenen Anwendungen, neue Compiler, einen neuen Zwischencode für ausführbare
2.1 Wozu .NET?
51
Programme, eine neue Entwicklungsumgebung, neue Programmiersprachen und zu guter Letzt eine neue Dokumentation. Dieses Mammutprojekt, das unter dem Kürzel .NET präsentiert wurde und wird, nahm mehrere Jahre in Anspruch.
Vorteile von .NET .NET verspricht, alle oben genannten Probleme zu lösen. Die DLL-Hell wird durch eine intelligentere Installation von Bibliotheken vermieden, die auch eine Parallelinstallation unterschiedlicher Versionen erlaubt, ohne dass diese sich im Weg sind. (Am einfachsten geht das dadurch, dass Bibliotheken in das Programmverzeichnis installiert werden.) Generell ist die Weitergabe von .NET-Programmen viel einfacher als die von COM-Programmen. Es ist nicht mehr notwendig, Bibliotheken in die Registrierdatenbank einzutragen. In vielen Fällen reicht es, die *.exe- und *.dll-Dateien einfach zu kopieren. (Vorausgesetzt wird dabei natürlich, dass am Zielrechner das .NET-Framework bereits installiert ist.) Das Problem der Speicherverwaltung wird dadurch gelöst, dass sich nicht mehr der Programmierer um die Freigabe von Objekten kümmert, sondern eine im Hintergrund laufende garbage collection. Das ist ein Prozess, der nicht mehr benutzte Objekte erkennt und aus dem Speicher entfernt. Die Sicherheitsprobleme werden dadurch gemindert, dass bei der gesamten Neukonzeption der .NET-Bibliotheken verschiedene Sicherheitsebenen beachtet wurden. Bestimmte Operationen dürfen nur dann ausgeführt werden, wenn die .NET-Sicherheitseinstellungen dies erlauben. Die Einstellung der Rechte, die .NET-Programme haben dürfen, erfolgt ähnlich wie beim Internet Explorer. Das erste Kriterium ist die Herkunft eines Programms. Aus dem Internet geladene Programme haben also weniger Rechte als von der lokalen Festplatte gestartete Programme. (Ob die neuen Sicherheitsmechanismen halten, was Microsoft zurzeit verspricht, muss sich natürlich erst erweisen!) Die neuen Klassenbibliotheken bieten mehr Konsistenz und deutlich bessere Möglichkeiten zur Lösung vieler (alltäglicher) Probleme. (Dass das Einlesen in die neuen Bibliotheken oft genauso lange dauert wie die umständliche Lösung eines bestimmten Problems mit alten Technologien, steht auf einem anderen Blatt.) Ein weiterer Vorteil von .NET besteht darin, dass das System weitgehend sprachunabhängig ist. Microsoft unterstützt neben den Sprachen C# und VB.NET auch C++ und J# (eine Java-ähnliche Programmiersprache). Drittanbieter unterstützen eine Reihe weiterer Programmiersprachen. Das .NET-Framework stellt durch vorgegebene Datentypen sicher, dass Komponenten problemlos zusammenarbeiten und Daten austauschen können, egal, mit welcher Sprache sie entwickelt wurden. Auch die Anforderungen zur Ausführung von .NET-Programmen sind unabhängig davon, mit welcher Sprache sie erstellt wurden. (Es kommen also immer dieselben Bibliotheken zum Einsatz.) Java-Programmierer werden beim Einlesen in die .NET-Dokumentation und speziell bei der Anwendung der Programmiersprache von C# vertraute Merkmale feststellen. Vieles, was .NET jetzt bietet, ist also gar nicht so neu, sondern stand Java-Programmierern in ähnlicher Form schon länger zur Verfügung. .NET ist sicherlich nicht einfach eine Kopie von
52
2 Das .NET-Universum
Java, aber es ist durchaus ein weiterer Beweis für die Fähigkeit Microsofts, gute Ideen der Konkurrenz aufzugreifen und zu adaptieren.
Nachteile von .NET Natürlich hat Microsoft schon bisher bei der Einführung jeder neuen Technologie das Blaue vom Himmel versprochen. Das hat sich auch mit .NET nicht geändert. So gut die Ideen sind, die hinter .NET stehen, so sehr ist doch eine gewisse Skepsis, ein gewisser Realismus angebracht! (Wenn man hin und wieder eine von Microsoft organisierte Entwicklerkonferenz besucht, kann dieser Realitätssinn ja leicht verloren gehen ...) Was momentan gegen .NET spricht, ist der Umstand, dass die .NET-Klassenbibliothek einfach noch zu neu ist. Diese Bibliothek umfasst Millionen Zeilen von Code. Trotz des mehrjährigen Betatests wäre es naiv zu glauben, dass dieser Code fehlerfrei ist. Zudem ist die Dokumentation vieler Funktionen – vor allem solcher, die sich etwas tiefer in der .NETBibliothek verstecken – noch ziemlich lückenhaft. (Es ist wohl jedes Schlüsselwort irgendwie beschrieben, aber oft bleiben der Kontext bzw. die vorgehesehene Technik für die konkrete Anwendung schleierhaft.) Ein weiteres Argument gegen .NET ist dessen Windows-Abhängigkeit. Zwar gibt es Versprechungen, zumindest Teile der .NET-Klassenbibliothek auch unter Unix/Linux zur Verfügung zu stellen, und zum Teil auch von Microsoft unabhängige Open-Source-Projekte, die in diese Richtung gehen; aber wieweit diese Projekte erfolgreich sein werden, bleibt abzuwarten. Ich wage zu bezweifeln, dass wirklich eine Kompatibilität erreicht wird, die so weit geht, dass ein unter Windows entwickeltes Programm ohne Änderungen auch unter Linux läuft. (Vor Jahren gab es auch Versprechungen, den Internet Explorer, die ActiveX-Technologie etc. auf Unix/Linux zu portieren. Auch wenn der gute Wille durchaus da war und nun eine alte IE-Version tatsächlich für einige Unix-Derivate zur Verfügung steht, blieben diese Portierungsversuche in einem sehr unvollkommenen Stadium stecken und waren letztlich nicht erfolgreich.) Ein riesiges Problem im Zusammenhang mit .NET ist die Wartung alten Codes. Die Durchführung eines neuen Projekts mit .NET mag eine Herausforderung sein, was die Einarbeitung in die neuen Konzepte betrifft, wirft aber ansonsten nicht mehr Probleme auf als sonst üblich. .NET ist aber grundsätzlich ungeeignet, vorhandenen COM-Code weiterzuentwickeln. Um die zum Teil riesigen VB6-Projekte zu warten, die in den letzten Jahren entstanden sind, müssen Sie weiter VB6 verwenden! .NET enthält zwar hochkomplexe Kompatibilitätsmechanismen zwischen COM und .NET (Schlagwort interoperabilty), aber sobald Sie in einem .NET-Programm auf COM zurückgreifen, verlieren Sie den Großteil der Vorteile von .NET. Es ist Microsoft somit nicht gelungen, einen plausible Migrationsweg anzubieten. Problematisch ist die Weitergabe von .NET-Programmen. Damit diese ausgeführt werden können, muss am Rechner des Kunden das .NET-Framework installiert werden. Das dafür erforderliche Installationsprogramm ist mehr als 20 MByte groß; auf der Festplatte beansprucht das .NET-Framework sogar deutlich mehr Platz. Windows 95 wird gar nicht mehr unterstützt, Windows NT4 nur, wenn alle aktuellen Service-Packs installiert sind etc.
2.2 Das .NET-Framework
53
Unklar ist schließlich die Situation für Programmierer, die basierend auf .NET InternetAnwendungen entwickeln möchten (ASP.NET, Web-Services). Das Problem stellt hier weniger die Technologie an sich dar, sondern die Tatsache, dass es noch sehr wenige Internet-Service-Provider gibt, die .NET unterstützen. Wenn Sie den Webserver also nicht selbst verwalten (was nur bei großen Firmen sinnvoll ist) und daher auf einen externen Server angewiesen sind, müssen Sie zuerst einmal einen Internet-Service-Provider finden, dem Sie so weit vertrauen, dass dieser die zugrunde liegende Technik und die Wartung der Server (hinsichtlich Sicherheits-Updates) wirklich im Griff hat. .NET ist ja nicht nur für Programmierer Neuland, sondern auch für alle, die darauf basierende Dienste anbieten. Lassen Sie sich von diesen Nachteilen aber nicht allzusehr abschrecken: Viele Probleme sind nicht grundlegender, sondern zeitlicher Natur, und werden sich in den nächsten ein bis zwei Jahren gewissermaßen von selbst lösen. .NET wird ausreifen (das erste Service Pack gab es schon, bevor dieses Buch fertig war), das .NET-Framework wird auf immer mehr Rechnern standardmäßig installiert sein, Internet-Service-Provider und andere Software-Firmen werden sich auf .NET einstellen etc. Ich erwarte, dass .NET das Fundament für die Software-Entwicklung unter Windows für die kommenden Jahre sein wird, und ich habe den Eindruck, dass es ein gutes Fundament ist. Aber spätestens in zehn Jahren wird sicher wieder alles neu gemacht. Dann wird Microsoft plötzlich die Nachteile von .NET in die Welt posaunen, um so die Vorteile des nächsten Technologieschritts zu vermarkten. Schon jetzt ist vorauszusehen, dass sich die Slogans bis zum Verwechseln denen ähneln werden, die jetzt .NET bewerben ...
2.2
Das .NET-Framework
Der Begriff .NET-Framework bezeichnet die Summe aller Bibliotheken und Komponenten, die erforderlich sind, um .NET-Programme auszuführen. Zum .NET-Framework gehört unter anderem ein so genannter Just-in-time-Compiler, der MSIL-Code in Maschinencode übersetzt (Details folgen im nächsten Abschnitt), sowie diverse Administrationswerkzeuge, mit denen beispielsweise die .NET-Sicherheitseinstellungen verändert werden können. Das .NET-Framework SDK (Software Development Kit) enthält zusätzlich zum .NET-Framework alle Entwicklerwerkzeuge, um VB.NET- oder C#-Programme zu entwickeln, eine Menge Beispiele sowie eine umfassende Dokumentation zum .NET-Framework. Das .NET-Framework ist (mit oder ohne SDK) kostenlos bei http://www.microsoft.com erhältlich und befindet sich auch auf der CD-ROM zum Buch. Wenn Sie nun aber glauben, dass Sie alles kostenlos bekommen, irren Sie (natürlich): Die VB.NET- bzw. VS.NET-Entwicklungsumgebung ist nicht Bestandteil des SDKs! Die im SDK enthaltenen Werkzeuge sind durchwegs Kommandozeilentools, die mühsam anzuwenden sind. Wenn Sie VB.NET-Programme effizient und komfortabel entwickeln möchten, einen Designer zum Zusammenstellen von Formularen oder einen Debugger bei der Fehlersuche einsetzen möchten etc., benötigen Sie eine Version der Entwicklungsumgebung (siehe Abschnitt 2.7).
54
2 Das .NET-Universum
Überblick über die wichtigsten .NET-Bibliotheken Wichige Klassenbibliotheken des .NET-Frameworks mscorlib.dll System.Array, System.Collections.*, System.Console, System.Environment, System.IO.*, System.Math, System.Random, System.Math, System.Runtime.InteropServices.*, System.Runtime.Serialization.*, System.Security.*, System.Text.*, System.Threading.*, System.Type
Das ist die wohl wichtigste Bibliothek. Sie enthält unter anderem alle Basisdatentypen (Double, String) sowie Methoden für grundlegende Operationen (Dateizugriff, Verwaltung von Feldern, Multithreading etc.). Jedes .NET-Programm nutzt automatisch diese Bibliothek.
System.dll System.IO.*, System.Net.*, System.Net.Sockets.*, System.Security.Permissions.*, System.Text.RegularExpressions.*, System.Timers.*
Die Bibliothek enthält weitere Basisklassen, z.B. zur Nutzung verschiedener Netzwerkprotokolle.
System.Data.dll System.Data.*, System.Data.OleDb.*, System.Xml.*
Die Bibliothek enthält Klassen zur Datenbankprogrammierung (ADO.NET).
System.Drawing.dll System.Drawing.*, System.Drawing.Imaging.*, System.Drawing.Printing.*, System.Drawing.Text.*
Die Bibliothek enthält Klassen zur Grafikprogrammierung und zum Drucken (GDI+).
System.Management.dll System.Management.*
Die Bibliothek gibt Zugriff auf die Funktionen der Windows Management Instrumentation (WMI).
System.Web.dll System.Web.*, System.Web.Mail.*, System.Web.Security.*, System.Web.SessionState*, System.Web.UI.*, System.Web.UI.HtmlControls.*, System.Web.UI.WebControls.*
Die Bibliothek enthält Klassen zur Programmierung von InternetAnwendungen (ADO.NET).
System.Windows.Forms.dll System.Windows.Forms.*
Die Bibliothek enthält Klassen zur Windows-Programmierung.
System.Xml.dll System.Xml.*, System.Xml.Schema.*, System.Xml.Serialization.*, System.Xml.XPath.*, System.Xml.Xsl.*
Die Bibliothek enthält Klassen zur Bearbeitung von XML-Daten.
Microsoft.VisualBasic.dll Microsoft.VisualBasic.*
Die Bibliothek enthält Klassen, die eine Grundkompatibilität zu VB6 herstellen.
2.3 Architektur
55
Die Tabelle auf der Vorseite gibt eine kurze (und natürlich unvollständige!) Inhaltsangabe für die wichtigsten .NET-Bibliotheken. Bei jeder Bibliothek werden jeweils ihr Name sowie die wichtigsten darin vorkommenden Klassennamen angegeben. (* bedeutet, dass es mehrere Klassen gibt, die mit der gleichen Namenskombination beginnt.) Die DLL-Dateien der Bibliotheken befinden sich im Verzeichnis Windows\Microsoft.NET\Framework\versionsnummer. Die Summe aller dieser Bibliotheken wird manchmal auch als Framework Class Library (FCL) oder Base Class Library (BCL) bezeichnet.
Voraussetzungen zur Ausführung von .NET-Programmen Damit .NET-Programme auf einem Rechner ausgeführt werden können, muss das .NETFramework installiert sein. Dazu ist wiederum ein einigermaßen aktuelles Betriebssystem erforderlich. Windows 95 wird explizit nicht mehr unterstützt, Windows NT 4 nur, wenn alle aktuellen Service-Packs installiert sind. Weitere Details zu den Versionsvoraussetzungen finden Sie in Abschnitt 18.3.2.
Voraussetzungen zur Entwicklung von .NET-Programmen Die Entwicklung von .NET-Programmen setzt das .NET-Framework SDK voraus. Das SDK kann nur unter Windows NT 4.0 SP6a, 2000 oder XP installiert werden. Windows 95, 98 und ME werden nicht unterstützt. Noch größer sind die Anforderungen, wenn Sie Internet-Anwendungen entwickeln möchten (ADO.NET, Web-Services). Dann brauchen Sie Zugang zu einem Rechner, auf dem die Internet Information Services (IIS) laufen. Das setzt wiederum die Windows-Versionen NT, 2000 oder XP Professional voraus. Windows XP Home kann die IIS dagegen nicht ausführen. Die IIS müssen nicht notwendigerweise am selben Rechner laufen, d.h., Sie können auch unter Windows XP Home entwickeln, wenn Sie Zugang zu einem anderen Rechner mit den IIS haben.
2.3
Architektur
VERWEIS
Der Platz reicht hier nicht aus, um einen vollständigen Überblick über die Technik hinter .NET zu geben. Stattdessen werden hier die wichtigsten Begriffe vorgestellt, die die Architektur von .NET beschreiben. Das sollte als erster Einstieg ausreichen und Ihnen helfen, vertiefende Literatur leichter zu verstehen. In der Online-Hilfe finden Sie eine ausführlichere Beschreibung der .NET-Architektur, wenn Sie nach Einblicke in .NET Framework suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpconinsidenetframework.htm
56
2 Das .NET-Universum
MSIL (Microsoft Intermediate Language) .NET-Programme werden grundsätzlich nicht zu Maschinencode kompiliert, sondern zu einem Zwischencode mit dem Namen MSIL. Zusammen mit dem MSIL-Code werden außerdem Metadaten gespeichert, die alle im Programm vorkommenden Klassen, Methoden etc. beschreiben. Das ist besonders bei Bibliotheken praktisch, weil sich diese gewissermaßen selbst beschreiben. Der Compiler kann außerdem exakt überprüfen, ob Methoden mit den richtigen Parametern aufgerufen werden etc. Erst bei der Ausführung eines Programms wird dieses vom JIT-Compiler (siehe den nächsten Abschnitt) in Maschinencode umgewandelt. MSIL ist vergleichbar mit dem Java-Bytecode oder mit dem P-Code früherer VB-Versionen. Ein wesentliche Vorteil, der für die Verwendung eines Zwischencodes spricht, ist die Plattformunabhängigkeit. Theoretisch könnte ein .NET-Programm also auch auf einem AppleRechner ausgeführt werden; tatsächlich scheitert das allerdings (noch) daran, dass das gesamte .NET-System, also insbesondere die Bibliotheken und der JIT-Compiler, momentan nur für Windows zur Verfügung steht. Wenn Sie sich den MSIL-Code eines .NET-Programms ansehen möchten, können Sie dazu das Programm ildasm.exe verwenden (siehe Abbildung 2.1), das Sie im Verzeichnis Programme\Microsoft Visual Studio .NET\FrameworkSDK\Bin finden. Mit dem Programm können Sie auch die Struktur von Bibliotheken erforschen. Dazu laden Sie eine beliebige .NET-Bibliothek; das Programm zeigt dann ähnlich wie der Objektbrowser der VB.NETEntwicklungsumgebung alle verfügbaren Klassen, deren Methoden und Eigenschaften etc. an (siehe auch Abschnitt 6.3).
Abbildung 2.1: Der MSIL-Disassembler
2.3 Architektur
57
Ein großer Nachteil des MSIL-Konzepts besteht darin, dass es denkbar einfach ist, den Code eines kompilierten .NET-Programms zu rekonstruieren. Im Internet steht dazu das kostenlose Programm Anakrino zur Verfügung. Damit können Sie ein beliebiges Programm laden. Das Programm übersetzt den MSIL-Code in beinahe lesbaren C#-Code. (Es fehlen eigentlich nur die Namen lokaler Variablen.) http://www.saurik.com/net/exemplar/
Abbildung 2.2 zeigt die Prozedur Form1_Load des Programms Hello Windows aus Abschnitt 1.2. Die Abbildung sollte klar machen, dass Sie in .NET-Programmen nichts verbergen können. (Im Prinzip galt das schon immer, aber so einfach war es noch nie, fremden Code zu lesen.) Ob Verschlüsselungsalgorithmen oder Kopierschutzfunktionen – der Code muss so programmiert sein, dass auch die Kenntnis des Codes den Schutzmechanismus nicht beeinträchtigen kann! Anakrino kann wie ildasm.exe auch auf die .NET-Bibliotheken angewendet werden. Wenn Sie wissen wollen, wie die ConcatArray-Methode der String-Klasse funktioniert, zeigt Anakrino Ihnen den kompletten C#-Code an. (Allerdings funktioniert das nicht für Methoden, die extern, also außerhalb der eigentlichen .NET-Bibliothek, realisiert sind. Gerade die .NET-Basisbibliotheken greifen zum Teil intensiv auf diverse Betriebssystemfunktionen zurück, die nicht als .NET-Code vorliegen.)
Abbildung 2.2: Anakrino
JIT (Just-in-Time-Compiler) Sobald Sie ein .NET-Programm (also die *.exe-Datei) ausführen, wird automatisch ein Justin-Time-Compiler gestartet. Der übersetzt die gerade benötigten Teile des MSIL-Code in Maschinencode, der auf dem vorliegenden Rechner optimal ausgeführt werden kann. Da die Übersetzung erst zu diesem Zeitpunkt erfolgt, ist es möglich, den Maschinencode für
58
2 Das .NET-Universum
den vorliegenden Prozessor zu optimieren. Wieweit das tatsächlich der Fall ist und ob sich dadurch wirklich Geschwindigkeitsvorteile ergeben, kann ich allerdings nicht abschätzen. Wichtig ist, dass der JIT-Compiler nicht einfach das ganze Programm sofort kompiliert, sondern immer nur die gerade benötigten Methoden. Deswegen sieht es so aus, als würden selbst große Programme praktisch verzögerungsfrei gestartet.
HINWEIS
Ein entscheidender Vorteil des JIT-Compilers besteht darin, dass während der Codeumwandlung eine Menge Sicherheitsüberprüfungen durchgeführt werden können: ob der MSIL-Code korrekt ist, ob keine unerlaubten Speicherzugriffe erfolgen, ob bei der Übergabe von Parametern an andere Bibliotheken die richtigen Typen verwendet werden etc. Dabei werden die .NET-Sicherheitseinstellungen beachten (siehe Abschnitt 2.4). Der JITCompiler stellt also einen wichtigen Teil des .NET-Sicherheitskonzeptes dar. Die Bibliotheken des .NET-Frameworks werden übrigens bereits bei dessen Installation von MSIL-Code zu Maschinencode kompiliert. Damit ist sichergestellt, dass diese sehr häufig benötigten Bibliotheken ohne Verzögerungen sofort zur Verfügung stehen.
Managed code versus unmanaged code Managed code (verwalteter Code) bedeutet, dass sich .NET um die Speicherverwaltung der
Objekte kümmert. Speicher für nicht mehr benötigte Objekte wird automatisch durch eine so genannte garbage collection wieder freigegeben (siehe auch Abschnitt 4.6.2). Managed code bietet darüber hinaus verschiedene Vorteile beim Debugging und im Hinblick auf die Sicherheit. In VB.NET erstellter MSIL-Code gilt dank der Ausführung durch die CLR immer als managed. Wenn Sie aber von Ihrem VB.NET-Programm COM-Komponenten nutzen oder API-Funktionen aufrufen, dann wird dieser Code unmanaged ausgeführt. .NET ist also nicht verantwortlich dafür, ob durch COM-Komponenten oder durch die API-Funktionen reservierter Speicher ordnungsgemäß wieder freigegeben wird.
Safe code versus unsafe code Safe code bedeutet, dass das Programm nicht direkt den Speicher manipulieren darf. VB.NET-Programme enthalten ausschließlich safe code, weil VB.NET keine Möglichkeiten bietet, über Zeiger auf den Speicher zuzugreifen. In C# sieht es anders aus: Dort kann eine Prozedur (Funktion) als unsafe deklariert werden; innerhalb dieser Prozedur darf das Programm dann Zeiger verwenden. Unsafe code kann bei manchen Anwendungen zu effizienterem Code führen. Er hat aber
den Nachteil, dass bei einem Programmierfehler die Gefahr besteht, dass Daten unzulässig überschrieben werden, was zu einer Korruption von Daten bzw. zu einem Absturz des Programms führen kann (daher die Bezeichnung unsafe). Aus diesem Grund darf unsafe code nur in der höchsten .NET-Sicherheitsstufe ausgeführt werden. (Anders formuliert: Die Verwendung von unsafe code reduziert die Anwendbarkeit eines Programms.)
2.3 Architektur
59
Assemblies, Global Assembly Cache (GAC) Vereinfacht ausgedrückt ist eine Assembly ein Programm (*.exe) bzw. eine Bibliothek (*.dll). Die exakte Beschreibung einer Assembly ist etwas komplizierter: Eine Assembly ist eine .NET-Ausführungseinheit. Das bedeutet, dass es sich um ausführbaren Code handelt (MSIL), der aber durchaus auf mehreren Dateien verteilt sein darf. Darüber hinaus enthält eine Assembly auch Metadaten, die den Inhalt beschreiben (verfügbare Klassen, Methoden, Typen etc.). Optional kann eine Assembly auch Ressourcen umfassen, also nicht ausführbare Daten (Text, Bilder, Sounddateien etc.). Selbst wenn eine Assembly aus mehreren Dateien besteht, bildet sie dennoch eine logische Einheit, die nur als Ganzes weitergegeben werden kann und die nur einen einzigen Einsprungpunkt hat. Viele Sicherheitsmechanismen gelten auf Assembly-Ebene. Der Datenaustausch innerhalb einer Assembly ist effizienter als über sie hinaus. Eine gesamte Assembly hat eine einheitliche Versionsnummer. Assemblies stellen einen Teil der .NET-Lösung für das so genannte DLL-Hell-Problem aus der COM-Vergangenheit dar: .NET unterstützt die parallele Installation unterschiedlicher Assembly-Versionen. Jedes .NET-Programm gibt intern an, auf welche Versionen externer Assemblies es zugreift. .NET kümmert sich darum, dass die richtige Version verwendet wird. (Es ist also durchaus möglich, dass zwei .NET-Programme, die gleichzeitig ausgeführt werden, unterschiedliche Versionen einer an sich gleichen .NET-Bibliothek nutzen.) Die Assemblies, die primär dafür gedacht sind, dass sie von vielen .NET-Anwendungen genutzt werden sollen, werden in dem so genannten global assembly cache (kurz GAC) installiert. Äußerlich ist der GAC einfach ein Verzeichnis (z.B. C:\WINNT\assembly). Es enthält unter anderem alle .NET-Basisbibliotheken. Beim Einfügen von Assemblies in den GAC werden spezielle Überprüfungen durchgeführt, um eine möglichst hohe Sicherheit zu gewährleisten.
HINWEIS
Selbst entwickelte Bibliotheken, die nur für das eine oder andere Programm eingesetzt werden, gehören normalerweise nicht in den GAC, sondern einfach in dasselbe Verzeichnis wie die *.exe-Datei des Programms. (Microsoft möchte mit dieser Empfehlung offensichtlich vermeiden, dass der GAC in wenigen Jahren genauso überfüllt ist wie jetzt das Windows-Systemverzeichnis.) Wenn Sie sich das Verzeichnis C:\WINNT\assembly im Windows-Explorer ansehen, sieht es so aus, als würden sich die Dateien direkt darin befinden. Das ist aber nicht der Fall – der Explorer stellt nur eine vereinfachte Sicht dar. In Wirklichkeit beginnen an dieser Stelle eine Menge Unterverzeichnisse, die unter anderem die Parallelinstallation unterschiedlicher Versionen einer Bibliothek ermöglichen. Wenn Sie sich die tatsächliche Verzeichnisstruktur ansehen möchten, können Sie z.B. das Programm EINGABEAUFFORDERUNG dazu verwenden.
60
2 Das .NET-Universum
Common Language Runtime (CLR) Der Begriff Common Language Runtime fasst zusammen, was zur Ausführung von .NET-Programmen erforderlich ist: Dazu zählen im Wesentlichen zwei Dinge: •
der JIT-Compiler sowie andere Tools, um MSIL-Code zu starten und in Maschinencode umzuwandeln, und
•
die .NET-Klassenbibliotheken.
Indem das .NET-Framework auf einem Rechner installiert wird, steht die CLR auf diesem Rechner zur Verfügung. (Beachten Sie, dass hier nicht das .NET-Framework SDK gemeint ist. Bei der CLR geht es ausschließlich um die Ausführung von .NET-Programmen, nicht um deren Entwicklung.) Die Besonderheit der CLR besteht darin, dass sie sprachunabhängig ist (daher common!): Egal, ob ein .NET-Programm mit VB.NET, C# oder einer beliebigen anderen .NET-Programmiersprache entwickelt wurde – die CLR reicht für die Ausführung aus. (Im Gegensatz dazu erforderten früher VB6-Programme eine andere Runtime-Umgebung als Programme, die mit Visual C++ entwickelt wurden!)
Common Type System (CTS)
VERWEIS
Das Common Type System (bzw. das allgemeine Typensystem laut Online-Hilfe) ist eine formale Beschreibung der Datentypen, die von .NET grundsätzlich unterstützt werden. Dazu zählen Wert- und Verweistypen, unterschiedliche Klassentypen (z.B. vererbbare oder nicht vererbbare), Strukturen, Felder (arrays), Aufzählungen (enums) etc. Es ist nicht erforderlich, dass jede .NET-Programmiersprache alle Elemente des CTS unterstützt. Die von VB.NET unterstützten Teile des CTS werden in diesem Buch vor allem in den Kapiteln 4 (Variablen- und Objektverwaltung) und 7 (Objektorientierte Programmierung) beschrieben.
Common Language Specification (CLS) Da nicht jede .NET-Programmiersprache alle Elemente der CTS unterstützt, definiert die Common Language Specification eine Teilmenge des CTS: Diese Teilmenge muss von jeder Programmiersprache unterstützt werden, die .NET-tauglich ist. In der Praxis ist die CLS insofern wichtig, als jede Bibliothek, die der CLS entspricht, von allen .NET-Programmiersprachen uneingeschränkt verwendet werden kann. Die CLS ist also nichts anderes als eine Aufzählung von Regeln, die eine Bibliothek einhalten muss, damit sie als CLS-kompatibel bezeichnet werden kann. Die Regeln betreffen ausschließlich die nach außen hin sichtbare Schnittstelle der Bibliothek, also die öffentlichen Methoden und Eigenschaften von Klassen. Was hinter dieser Schnittstelle vor sich geht, ist für die CLS nicht relevant.
2.4 Sicherheitsmechanismen
61
HINWEIS
Zahlreiche Bücher behaupten, dass mit VB.NET erzeugte Bibliotheken automatisch CLS-kompatibel sind. Das ist falsch. Auch wenn VB.NET als Standardvariablentypen nur CLS-kompatible Typen unterstützt (siehe Abschnitt 4.2.6), können CLSinkompatible Typen (z.B. UInt16, UInt32 etc.) sehr wohl verwendet werden – und zwar auch bei der Definition von öffentlichen Klassenmitgliedern. Was vielleicht noch schlimmer ist: Der VB.NET-Compiler gibt nicht einmal eine Warnung von sich, wenn Sie eine CLS-inkompatible Bibliothek erstellen. In der Online-Hilfe zum Attribut CLSCompliant können Sie dazu lesen: Der aktuelle Microsoft Visual Basic-Compiler generiert bewusst keine CLS-Kompatibilitätswarnungen. Künftige Versionen des Compilers werden diese Warnung allerdings ausgeben. Eine Begründung, warum der Compiler keine deartigen Warnungen ausgibt, fehlt freilich. Der C#Compiler ist dem VB.NET-Compiler in diesem Punkt auf jeden Fall überlegen. ms-help://MS.VSCC/MS.MSDNVS.1031/cpref/html/frlrfsystemclscompliantattributeclasstopic.htm
2.4
Sicherheitsmechanismen
Das .NET-Framework und insbesondere die .NET-Bibliotheken implementieren ein ebenso feinmaschiges wie komplexes (um nicht zu sagen kompliziertes) Sicherheitskonzept. Vor der Ausführung jeder einzelnen Methode wird überprüft, ob das Programm bzw. der Benutzer, der das Programm ausführt, dazu ausreichende Rechte hat. Wenn das nicht der Fall ist, tritt ein Fehler auf. Welche Rechte ein Programm hat, hängt von mehreren Faktoren ab: •
Gewöhnliche Benutzerrechte: Bei den Windows-Versionen NT, 2000 und XP haben Benutzer je nach Login und Konfiguration unterschiedliche Rechte. Im Regelfall hat der Administrator uneingeschränkte Rechte, während alle anderen Benutzer nur ihre eigenen Dateien lesen und verändern, die Windows-Verzeichnisse aber nicht verändern dürfen etc. Neben diesen offensichtlichen Punkten betreffen die Login-abhängigen Rechte aber auch Details, die im normalen Betrieb seltener auffallen: Das Recht, andere Prozesse zu manipulieren, das Recht, Netzwerkverbindungen zu erstellen, das Recht, auf Windows-Ressourcen zuzugreifen, Daten in der Registrierdatenbank zu lesen und zu verändern etc. Diese Rechte wirken sich natürlich auf alle Operationen aus, die durch ein .NET-Programm durchgeführt werden. (Insofern ist der .NET-Sicherheitsmechanismus nicht neu; dieselben Einschränkungen galten auch schon für herkömmliche Programme.)
•
Herkunft des .NET-Programms: .NET unterscheidet zwischen Programmen von der lokalen Festplatte (am sichersten), aus dem lokalen Netzwerk (Intranet), aus dem Internet (am unsichersten) und von explizit als vertrauenswürdig bezeichneten Orten. Falls Sie sich schon einmal die Sicherheitseinstellungen des Internet Explorers angesehen haben, werden Ihnen diese vier Zonen auf Anhieb bekannt vorkommen.
62
2 Das .NET-Universum
Für jede dieser vier Zonen gelten unterschiedliche Defaultrechte. Diese Rechte steuern die so genannte Codezugriffssicherheit, also welche .NET-Methoden ausgeführt werden dürfen oder nicht. Die Codezugriffssicherheit ist eine wesentliche Neuerung im .NET-Sicherheitssystem. .NET-intern werden die Zonen durch so genannte Codegruppen beschrieben. Die für eine Codegruppe zulässigen Rechte werden wiederum durch Berechtigungssätze ausgedrückt. (Ein Berechtigungssatz ist einfach eine Aufzählung von Rechten.) •
ACHTUNG
.NET-Sicherheitskonfiguration: Das .NET-Sicherheitssystem kann auf drei Ebenen konfiguriert werden: für eine ganze Organisation (z.B. auf allen Rechnern einer Firma), für einen Rechner oder für einen Benutzer. Als Entwickler sollten Sie bedenken, dass die Frage im Allgemeinen nicht lautet, ob ein Programm ausgeführt werden darf oder nicht, sondern welche Teile des Programms ausgeführt werden dürfen! Oft kann ein Programm ohne weiteres gestartet werden; irgendwann während der Ausführung versucht das Programm dann, eine Methode aufzurufen, die das .NET-Framework als nicht zulässig betrachtet. Es tritt nun ein Fehler (eine SecurityException) auf; wenn das Programm gegenüber diesem Fehler nicht abgesichert ist (siehe Kapitel 11), wird es beendet. Besonders unangenehm ist der Umstand, dass Sie derartige Probleme nicht bemerken, wenn Sie wie viele Entwickler unter Windows als Administrator arbeiten und sich Ihre Programmdateien auf der lokalen Festplatte befinden. Dann haben Sie bei der probeweisen Ausführung Ihrer Programme beinahe uneingeschränkte Rechte, und alles funktioniert wunderbar. Erst wenn Sie die Programmausführung mit eingeschränkten Rechten ausprobieren, werden Sie feststellen, wo Ihr Programm auf Sicherheitsprobleme stoßen kann.
Konfiguration des Sicherheitssystems Das .NET-Sicherheitssystem wird durch drei XML-Dateien gesteuert, die die Codezugriffsrechte auf drei Ebenen steuern. (Die im Folgenden angegebenen Pfade sind natürlich abhängig von der Windows-Installation und der .NET-Version. WINNT bezeichnet das Windows-Verzeichnis, Dokumente und Einstellungen das Verzeichnis für Benutzerdaten. v1.0.3705 ist die Build-Nummer der .NET-Framework-Version.) •
Unternehmensebene (Organisation, Enterprise): WINNT\Microsoft.NET\Framework\v1.0.3705\config\enterprisesec.config
Defaulteinstellung: jeder darf alles •
Rechnerebene (Computer): WINNT\Microsoft.NET\Framework\v1.0.3705\config\security.config
Die Defaulteinstellung ist zonenabhängig:
2.4 Sicherheitsmechanismen
63
Lokaler Rechner (Arbeitsplatz): jeder darf alles (Berechtigungssatz FullTrust) Intranet: eingeschränkte Rechte (Berechtigungssatz LocalIntranet) Vertrauenswürdige Sites: noch stärkere Einschränkungen (Berechtigungssatz Internet) Internet: alles ist verboten (Berechtigungssatz Nothing) •
Benutzerebene: Dokumente und Einstellungen\benutzer\Anwendungsdaten\Microsoft\ CLR Security Config\v1.0.3705\security.config
Defaulteinstellung: jeder darf alles Auf einer oberen Ebene eingeschränkte Rechte können auf den unteren Ebenen weiter eingeschränkt, aber nicht mehr erweitert werden. Die Defaulteinstellung bewirkt, dass alle Zugriffsrechte durch die Rechnerebene bestimmt werden und nur auf dieser Ebene erweitert werden können. Ein gewöhnlicher Benutzer kann sich damit selbst nicht mehr Rechte zuweisen, als der Administrator des Rechners auf Computer-Ebene zulässt. Zur Einstellung der Rechte ist das .NET-Konfigurationsprogramm vorgesehen. Dieses Programm wird mit SYSTEMSTEUERUNG|VERWALTUNG|MICROSOFT .NET FRAMEWORK KONFIGURATION gestartet (siehe Abbildung 2.3). Die direkte Veränderung der Rechte mit diesem Programm setzt allerdings noch ein detaillierteres Verständnis der Mechanismen zur Zuordnung der Rechte voraus als hier vermittelt werden konnte. Eine rudimentäre Veränderung der Rechte erlaubt aber der in den folgenden Absätzen beschriebene Assistent.
Einstellung der Zonensicherheit Das größte Problem am .NET-Sicherheitssystem sehe ich in dem Umstand, dass das System viel zu komplex ist. So wie es unzählige Windows-Anwender gibt (ich nehme mich selbst nicht aus), die unter Windows NT/2000/XP grundsätzlich als Administrator arbeiten, weil es zu umständlich ist, einen sicheren Account einzurichten, der gleichzeitig ausreichend Rechte für die alltägliche Arbeit gibt, so werden sich auch die meisten (Privat)Anwender nicht mit den Details der .NET-Sicherheit beschäftigen. Immerhin bietet das .NET-Sicherheitssystem zumindest in Firmennetzen die Möglichkeit, die Sicherheitseinstellungen zentral zu administrieren. Microsoft hat dieses Problem anscheinend vorhergesehen und stellt einen einfachen Assistenten zur Verfügung, mit dem die Zonensicherheit relativ grob konfiguriert werden kann (siehe Abbildung 2.3). Um den Assistenten zu starten, führen Sie SYSTEMSTEUERUNG|VERWALTUNG|MICROSOFT .NET FRAMEWORK KONFIGURATION aus und klicken dann DIE ZONENSICHERHEIT ANPASSEN an. Sie können nun die gewünschte Vertrauensebene für die vier Zonen Arbeitsplatz, Lokales Intranet, Internet und Vertrauenswürdige Sites angeben. Diese Einstellung kann ein gewöhnlicher Anwender nur für sich selbst, der Administrator aber auch für den ganzen Rechner durchführen. (Welche Adressen als vertrauenswürdig gelten, können Sie übrigens im Optionsdialog des Internet Explorers einstellen.) Das große Problem mit dem Assistenten besteht darin, dass er dazu verleitet, bei SecurityException-Problemen einfach die Vertrauensebene zu erhöhen. Wenn also die Ausführung
64
2 Das .NET-Universum
eines Programms aus dem Intranet aufgrund unzureichender Rechte scheitert, deklariert der Administrator das Intranet im Assistenten als voll vertrauenswürdig. Dann läuft zwar das Programm, aber auch alle anderen .NET-Programme aus dem Intranet haben nun uneingeschränkte Rechte (soweit diese nicht durch den Login beschränkt werden). Das gesamte, ausgeklügelte Sicherheitssystem ist damit ad absurdum geführt und alles ist so unsicher wie eh und je. Und genau so wird es wohl auch kommen – eben weil eine korrekte und sichere Konfiguration so kompliziert ist, dass eine vollständige Erklärung ein ganzes Kapitel, wenn nicht ein ganzes Buch füllen würde.
VERWEIS
Abbildung 2.3: Einstellung der .NET-Zonensicherheit
Einführende Informationen dazu, wie Sicherheitsverletzungen im Programmcode abgesichert bzw. getestet werden können, gibt Abschnitt 12.3. Weitere Informationen zum Thema Sicherheit finden Sie in der Online-Hilfe, wenn Sie nach Sichern von Anwendungen bzw. nach Konfigurieren der Sicherheitsrichtlinien suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpconsecuringyourapplication.htm ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpconsecuritypolicyconfiguration.htm
2.5 .NET und das Internet
2.5
65
.NET und das Internet
Wenn man die Berichterstattung über .NET in den Medien verfolgt hat, konnte man leicht den Eindruck gewinnen, .NET habe ausschließlich mit dem Internet zu tun. Das ist falsch! .NET bietet zahllose Verbesserungen für jede Art der Anwendungsentwicklung, unabhängig davon, ob herkömmliche Windows-Anwendungen, neue Komponenten oder InternetAnwendungen entwickelt werden sollen. Microsoft hätte sich sicher nicht so viel Mühe gegeben, riesige .NET-Bibliotheken zur Windows- und Grafikprogrammierung zu entwickeln, wenn es einzig um die Programmentwicklung für das Internet ginge. Richtig ist aber natürlich, dass .NET unter dem Gesichtspunkt entwickelt wurde, die Entwicklung gerade von Internet-Anwendungen wesentlich zu verbessern. Auch wenn dieses Buch den Schwerpunkt bei VB.NET-Grundlagen und der herkömmlichen Windows-Programmierung setzt (die Themen Datenbanken und Internet werden in einem eigenen Buch beschrieben), sind daher einige Worte zum Thema Internet angebracht (und sei es nur, um eine Erklärung für die Begriffe zu geben, auf die Sie in der sonstigen Literatur unweigerlich stoßen).
ASP.NET ASP.NET ist der Nachfolger von ASP (Active Server Pages). ASP ermöglicht die Entwicklung dynamischer Webseiten auf der Basis von VBScript-Code. Obwohl ASP technologisch gesehen eine schreckliche (weil unsichere und ineffiziente) Technologie ist, hat sie sich in einem Ausmaß durchgesetzt, das vermutlich nicht einmal Microsoft erwartet hat. Der Grund besteht darin, dass ASP einfach und flexibel ist. ASP.NET ist nun der Nachfolger zu ASP und zeichnet sich durch viele Vorteile aus. Die beiden wichtigsten sind: •
ASP.NET-Code kann nun mit der VB.NET-Entwicklungsumgebung entwickelt werden.
•
ASP.NET-Code wird kompiliert. (ASP-Code wurde interpretiert, was deutlich ineffizienter ist.)
Im Vergleich zu den in VB6 eingeführten WebClasses (die in VB.NET nicht mehr unterstützt werden), zeichnet sich ADO.NET vor allem dadurch aus, dass fertige Lösungen einfach durch ein Kopieren der Dateien in ein Verzeichnis installiert werden können (xcopyInstallation). Vom Standpunkt des Entwicklungskomforts und der angebotenen Möglichkeiten ist ASP.NET ein Meilenstein (nicht nur im Vergleich zu ASP, sondern auch zu konkurrierenden Technologien wie PHP). Der größte Nachteil besteht darin, dass ASP.NET eine teure Technologie ist: Als Internet-Server muss ein Windows-Server-Betriebssystem mit den Internet Information Services eingesetzt werden, in der Regel in Kombination mit dem SQL Server als Datenbank. Die Lizenzgebühren für einen derartigen Server sind hoch, insbesondere im Vergleich zu einem LAMP-System (Linux + Apache + MySQL + PHP/Perl). Insofern bietet sich ASP.NET eher für große und professionelle Websites an, bei denen die Lizenzkosten nur eine untergeordnete Rolle spielen. Die kostenlos verfügbare Entwicklungsumgebung ASP.NET Web Matrix (http://www.asp.net/webmatrix/) ermöglicht zwar ein kostengünstiges Ausprobieren von ASP.NET, mindert aber die Weitergabekosten nicht.
66
2 Das .NET-Universum
Web-Services Web-Services ermöglichen eine standardisierte Kommunikation zwischen Internet-Servern über das HTTP-Protokoll. Im Gegensatz zu ADO.NET erfolgt der Datenaustausch aber nicht im HTML-, sondern im XML-Format. Web-Services sind beispielsweise dann praktisch, wenn ein Internet-Server keine fertigen Webseiten, sondern lediglich Daten anbietet (z.B. Börsenkurse). An sich sind Web-Services eine gute Idee. Wirklich neu daran ist aber nur die Standardisierung des Datenaustauschs. Schon bisher konnten Daten über das HTTP-Protokoll oder über andere Protokolle über das Internet ausgetauscht werden. Neben Microsoft bieten auch andere Firmen (Sun, IBM etc.) Technologien an, die ähnliche Funktionen wie Microsofts Web-Services bieten. Da diese Technologien zum Teil auf denselben Standards basieren (XML, SOAP), sind sie sogar miteinander kompatibel; wieweit der Datenaustausch dann auch in der Praxis funktioniert, wird sich erst dann zeigen, wenn Web-Services einen größeren Stellenwert im Internet einnehmen.
.NET MyServices Zu den ersten konkreten Anwendungen von Web-Services zählt .NET MyServices: Die Grundidee dieses Systems (das früher den Namen Hailstorm hatte) besteht darin, dass kundenspezifische Daten – Name, Adresse, optional sogar Kreditkartendaten etc. – zentral bei Microsoft gespeichert werden. Wenn der Kunde nun bei einem .MyServices-Kooperationspartner etwas bestellen möchte, würden so viele Daten weitergegeben, dass eine (hoffentlich sichere) Transaktion möglich wird. Der Vorteil für den Kunden besteht darin, dass es nicht mehr notwendig ist, vor jedem Internet-Kauf eine Registrierung samt der Angabe von Adresse, Kreditkarteninformationen etc. durchzuführen. Ein zentraler .MyServices-Login würde ausreichen, egal ob bei der Firma X ein Flugticket, bei der Firma Y ein Buch oder beim Broker Z ein paar Aktien gekauft werden sollen. Der Vorteil für .MyServices-Kooperationspartner (also für Firmen): Sie könnten auf ein standardisiertes Konzept zum Austausch dieser sicherheitskritischen Daten zurückgreifen. Zudem würde die Hemmschwelle für Internet-Einkäufe reduziert: Wer sich einmal bei MyServices angemeldet hat, kann sofort einkaufen. Die Idee klingt prinzipiell faszinierend und praxisnah, allerdings schrillten bei allen Datenschützern die Alarmglocken: Die zentrale Speicherung derart wichtiger Daten ausgerechnet bei einem Unternehmen mit Monopolcharakter erscheint wenig erstrebenswert. Bedenklich stimmt auch, dass das Konzept .NET MyServices laut einem Bericht der New York Times (April 2002) offensichtlich auf geringe Resonanz bei anderen Firmen gestoßen ist. Es bleibt also abzuwarten, ob .NET MyServices ein Erfolg beschert sein wird. http://www.microsoft.com/myservices/
.NET Passport Die .NET-Passport-Funktion ist ein zentrales Login-Service und insofern der erste Schritt zu .NET MyServices. .NET Passport ermöglicht es, nach einem zentralen Login auf mehre-
2.6 Programmiersprachen (C# versus VB.NET)
67
re unterschiedliche Websites (die natürlich mit .NET Passport koopieren müssen) zuzugreifen. Passport ist im Gegensatz zu MyServices bereits Realität, weil beispielsweise jeder Hotmail-Login via Passport erfolgt. Bis jetzt gibt es aber fast keine Microsoft-unabhängigen Websites, die einen Passport-Login akzeptieren.
2.6
Programmiersprachen (C# versus VB.NET)
Zur .NET-Programmierung stehen Ihnen – zumindest wenn man den Microsoft-Versprechungen trauen darf – Dutzende von Programmiersprachen zur Auswahl. Die zwei wichtigsten .NET-Sprachen sind aber ohne jeden Zweifel VB.NET und C#. Welche dieser Sprachen ist nun besser? Da Sie dieses Buch lesen und nicht ein C#-Buch, vermute ich, dass die Entscheidung für Sie schon gefallen ist. Wenn Sie aber noch unschlüssig sind, hier einige Anmerkungen: •
Die beiden Sprachen sind zu 99 Prozent gleichwertig. Die Syntax sieht zwar anders aus, aber Sie können dieselben Dinge erzielen. (Die verbleibenden Unterschiede folgen gleich.)
•
Sie können beide Sprachen zur Entwicklung eines Programms mischen. Dazu muss das Programm aus mehreren Teilprojekten zusammengesetzt werden. Damit kann theoretisch jeder Entwickler seine Lieblingssprache verwenden. In der Praxis wird das aber kaum sinnvoll sein, weil die Wartung des Codes natürlich umso komplizierter wird, je mehr Sprachen das Projekt einsetzt.
C#-Vorteile •
C# unterstützt das Überladen von Operatoren, Integerzahlen ohne Vorzeichen und die direkte Dokumentierung von Quellcode. (Alle drei Merkmale werden für künftige Versionen auch für VB.NET versprochen.)
•
C# bietet die Möglichkeit, unsafe code zu entwickeln, um über Zeigern direkt auf den Speicher zuzugreifen. Das kann in manchen Anwendungen Geschwindigkeitsvorteile mit sich bringen.
•
Microsoft-intern wird C# offensichtlich favorisiert. Viele Microsoft-Projekte werden momentan mit C# durchgeführt. Daher ist zu erwarten, dass Fehler in C# früher entdeckt und behoben werden als in VB.NET. Bemerkenswert ist auch, dass die OnlineHilfe zur .NET-Klassenbibliothek viel mehr C#- als VB.NET-Beispiele enthält.
•
Microsoft hat C# zur Standardisierung durch ein unabhängiges Gremium freigegeben (durch die ECMA, das ist die European Computer Manufacturer's Association). Das macht es anderen Software-Herstellern möglich, ebenfalls einen C#-Compiler zu entwickeln. Es bleibt aber abzuwarten, ob es tatsächlich je möglich sein wird, ein normales C#-Programm ohne größere Änderungen auf einer anderen Plattform zu kompilieren. Das Mono-Projekt (http://www.go-mono.net/) versucht momentan, Teile des .NET-Frameworks als kostenlose Software für Linux zu entwickeln. Das Projekt umfasst unter
68
2 Das .NET-Universum
anderem einen C#-Compiler, der grundsätzlich bereits läuft, sowie wichtige .NET-Bibliotheken. Allerdings sind nur kleine Teile der .NET-Klassenbibliothek ebenfalls der ECMA zur Standardisierung übergeben werden. Die Portierung der .NET-Bibliotheken bewegt sich daher auf unsicherem Boden. •
C# kann anders als VB.NET dazu verwendet werden, so genannten unsafe code zu entwickeln (etwa zum direkten Speicherzugriff durch pointer). Das bietet manchmal die Möglichkeit, besonders systemnahen und effizienten Code zu gestalten.
•
Wer bisher mit C, C++, Java oder PHP programmiert hat, wird sich schließlich mit der C#-Syntax leichter tun als mit der von VB.NET.
•
Schließlich verspricht das C von C# Professionalität, während das Basic in VB.NET von vielen weiterhin so gedeutet wird, dass es sich hier um eine Programmiersprache für Kinder handelt ... Dieses Mißverständnis wird aus den Köpfen mancher Leute sicher nie verschwinden.
VB.NET-Vorteile •
VB.NET unterstützt optionale Parameter mit Defaultwerten sowie das Schlüsselwort With, mit dem der Objektzugriff im Code in manchen Fällen vereinfacht werden kann. (Beide Merkmale fehlen in C#.)
•
VB.NET-Code ist unabhängig von der Groß- und Kleinschreibung.
•
Der VB.NET-Editor bietet deutlich mehr Komfort als der C#-Editor: Die automatische Codeeinrückung bei Änderungen der Programmstruktur funktioniert viel besser, die Groß- und Kleinschreibung wird automatisch richtig gestellt, fehlerhafter Code wird meist schon während der Eingabe erkannt (weil der Code im Hintergrund kompiliert wird) etc. Das Gegenargument, dass der Tippaufwand bei VB.NET größer ist als bei C#, ist zwar richtig, aber irrelevant. Haben Sie schon mal überlegt, wie groß der Zeitaufwand für das Eingeben von 100 Zeilen Code ist? Und wie groß der Aufwand ist, um diesen Code zu entwicken, die darin enthaltenen Fehler zu suchen? Die Eingabezeit ist dagegen vollkommen vernachlässigbar!
•
Manche Einschränkungen in VB.NET gegenüber C# kommen der Sicherheit, Kompatibilität und Stabilität der resultierenden Programme zugute. Insofern kann man es durchaus als Vorteil betrachten, dass VB.NET keine Integer-Datentypen ohne Vorzeichen unterstützt (diese Datentypen sind nicht CLS-kompatibel) oder dass VB.NET die Verwendung von Zeigern grundsätzlich verbietet.
•
Für VB.NET spricht die ausführlichere, besser lesbare Syntax. Das macht das Lesen von fremdem Code deutlich einfacher als bei C# und ist ein wesentlicher Vorteil, wenn Code von unterschiedlichen Personen bearbeitet wird.
•
Wer bereits Erfahrungen mit VB1-6, VBA, VBScript, WSH oder ASP hat – und das sind Millionen von Programmierern! – wird sich in VB.NET-Code ohne große Umstellungsprobleme zurechtfinden. In diesem Zusammenhang hilft vor allem die VB.NET-Run-
2.7 Entwicklungsumgebungen
69
VERWEIS
time-Bibliothek, die zahlreiche aus VB vertraute Funktionen unter VB.NET zur Verfügung stellt: die String-Funktionen Left, Mid und Right, Konstanten wie vbCrLF, Hilfsfunktionen wie IIf etc. (Grundsätzlich können Sie diese Bibliothek auch in C# nutzen, das ist aber unüblich.) Im Internet sind zurzeit zwei Konverter von C# zu VB.NET zu finden. Konverter in die umgekehrte Richtung scheint es noch nicht zu geben. http://www.kamalpatel.net http://www.aspalliance.com/aldotnet/examples/translate.aspx
Fazit Es gibt momentan keine zwingenden Argumente, die belegen, dass die eine oder andere Sprache besser ist als die andere. Es lässt sich auch noch nicht abschätzen, ob die Mehrheit der Programmierer eher zu C# oder zu VB.NET tendiert. Verwenden Sie also die Sprache, die Ihnen sympatischer ist. (Und wenn Sie dieses Buch lesen möchten: Verwenden Sie VB.NET!)
2.7
Entwicklungsumgebungen
Wie in Abschnitt 2.2 bereits erwähnt wurde, sind grundsätzlich alle Werkzeuge, die zur Entwicklung von VB.NET-Programmen unbedingt erforderlich sind, kostenlos verfügbar. Das Problem besteht nur darin, dass diese Werkzeuge so umständlich zu bedienen sind, dass sofort der Wunsch nach einer komfortablen Entwicklungsumgebung aufkommt.
HINWEIS
Microsoft bietet eine ausgezeichnete, aber leider relativ teure Entwicklungsumgebung im Rahmen seiner VB.NET- bzw. VS.NET-Produkte an. Wer Geld sparen will, kann stattdessen die kostenlose Entwicklungsumgebung SharpDevelop einsetzen oder eben ganz ohne Entwicklungsumgebung arbeiten. Dieser Abschnitt stellt alle drei zurzeit aktuellen Optionen kurz vor. (Möglicherweise wird es in Zukunft noch mehr kommerzielle oder kostenlose Alternativen geben.) Beachten Sie, dass jede .NET-Programmentwicklung – egal ob mit oder ohne Entwicklungsumgebung – Windows NT, 2000 oder XP erfordert! Die Beispiele dieses Buchs setzen voraus, dass Sie mit der Entwicklungsumgebung von Microsoft arbeiten. VB.NET Standard reicht für die meisten Beispiele aus, professionelle Programmierer sind mit VS.NET Professional aber sicherlich besser beraten.
70
2 Das .NET-Universum
2.7.1
Microsoft-Entwicklungsumgebungen
Eine Microsoft-Entwicklungsumgebung für VB.NET (siehe Abbildung 1.2 im vorigen Kapitel) können Sie im Rahmen einer ganzen Reihe von Versionen erwerben: •
VB.NET-Standard: Diese Minimalversion kann nur für VB.NET-Projekte verwendet werden, nicht aber für C#. (Für C#-Entwickler gibt es eine eigene Version C#-Standard.) Ich konnte diese Version selbst leider weder testen, noch fand ich auf den Seiten von http://www.microsoft.com eine wirklich präzise Beschreibung, welche Komponenten von VS.NET Professional (siehe unten) nun mit VB.NET Standard mitgeliefert werden. Laut Berichten aus verschiedenen Newsgruppen unterliegt die Standardversion aber massiven Einschränkungen, was die Unterstützung verschiedener Projekttypen betrifft. Insbesondere ist es nicht möglich, mit VB.NET-Standard eigene Klassenbibliotheken zu entwickeln. Alle Projekte müssen zu *.exe-Dateien kompiliert werden. *.dll-Kompilate werden nicht unterstützt.
•
VS.NET Professional: Für die meisten professionellen Entwickler ist das die am besten geeignete Version. Es werden prinzipiell alle Projekttypen unterstützt, d.h., es können sowohl VB.NET- als auch C#- und C++-Projekte durchgeführt werden. Im Vergleich zur Standardversion wird unter anderem der Migrationsassistent, das Datenbankberichtsystem Crystal Reports und ein umfassenderes Hilfesystem mitgeliefert.
•
VS.NET Enterprise: Diese Version enthält alle Komponenten von VS.NET Professional, zusätzlich den Visual Studio Analyzer, Visual Source Safe sowie Entwicklerlizenzen für Microsoft Windows 2000 Advanced Server, SQL Server 2000, Exchange Server, Host Integration Server und Commerce Server.
•
VS.NET Enterprise Architect: Hier bestehen die weiteren Ergänzungen unter anderem aus einer Visio-2002-Version zum interaktiven Datenbankdesign und einer Entwicklerversion des Microsoft BizTalk Servers.
Die mitgelieferte Entwicklungumgebung ist im Prinzip bei allen Versionen die gleiche. Die Unterschiede betreffen nur die Anzahl der in die Umgebung integrierten Designer, die mitgelieferten Zusatztools, den Umfang der Dokumentation, eventuell das Angebot für kostenlose Support-Anfragen etc.
TIPP
Beachten Sie, dass Sie VS.NET auch im Rahmen eines MSDN-Abonnements erwerben können. Das ist dann empfehlenswert, wenn Sie an einem Gesamtpaket verschiedener Microsoft-Produkte interessiert sind. Es ist zu hoffen, dass es demnächst auch preisreduzierte VB.NET- bzw. VS.NETVersionen für Schüler, Studenten und generell für den akademischen Bereich geben wird. Für den US-Markt gibt es bereits derartige Angebote; im deutschen Sprachraum habe ich bis zur Manuskriptabgabe allerdings noch keine vergleichbaren Versionen entdecken können.
VERWEIS
2.7 Entwicklungsumgebungen
71
Wenn Sie VS.NET schon besitzen, finden Sie in der Online-Hilfe hier eine Beschreibung der verschiedenen VS.NET-Versionen: ms-help://MS.VSCC/MS.MSDNVS.1031/vbcon/html/vbgrfvisualbasicstandardeditionfeatures.htm
Im Internet finden Sie einen relativ ausführlichen Produktvergleich hier: http://msdn.microsoft.com/vstudio/howtobuy/choosing.asp
2.7.2
SharpDevelop
SharpDevelop ist eine in C# programmierte Entwicklungsumgebung für C#- und VB.NETProjekte (siehe Abbildung 2.4). Zum Zeitpunkt der Manuskriptabgabe lag das Programm in einer bereits brauchbaren, aber nicht ganz stabilen Betaversion 0.88b vor. (Diese Version finden Sie auch auf der CD zu diesem Buch.) http://www.icsharpcode.net/OpenSource/SD/default.asp
Die vielleicht größte Besonderheit von SharpDevelop besteht darin, dass es im Rahmen der GNU Public License (GPL) kostenlos und samt Quellcode zur Verfügung steht. Die wichtigste Auflage der GPL besteht darin, dass Sie eine weiterentwickelte Version von SharpDevelop nur dann verbreiten dürfen, wenn Sie Ihre Änderungen am Quellcode kostenlos zur Verfügung stellen. (Mit anderen Worten: Sie dürfen SharpDevelop nicht zu einer kommerziellen Entwicklungsumgebung ausbauen, wenn Sie nicht bereit sind, alle Änderungen am Code kostenlos zur Verfügung zu stellen.)
Abbildung 2.4: VB.NET-Programmentwicklung mit SharpDevelop
72
2 Das .NET-Universum
Was kann SharpDevelop? Zu den Grundaufgaben einer Entwicklungsumgebung gehört die Verwaltung von Projektund Codedateien, die bereits ausgezeichnet funktioniert. Die Codeeingabe wird durch eine farbige Syntaxhervorhebung und durch einige Assistenten unterstützt, mit denen Codeschablonen eingegeben werden können. SharpDevelop kann nicht nur zur Eingabe von C#- und VB.NET-Code verwendet werden, sondern unterstützt unter anderem auch die Formate HTML und XML. Beim Kompilieren des Codes zeigt das Programm eine Fehlerliste an. Per Doppelklick können Sie in die betreffende Zeile springen.
Was kann SharpDevelop (noch) nicht? Im Vergleich zur Microsoft-Entwicklungsumgebung fehlen SharpDevelop noch viele Funktionen. Die folgende Liste basiert auf der SharpDevelop-Version 0.88b und nennt nur die wichtigen Punkte: •
Es gibt keinen Designer zum Entwurf von Windows.Forms-Formularen.
•
Es gibt keinen Debugger.
•
F1 führt nur zur Online-Hilfe von SharpDevelop, nicht aber zum Hilfetext des gerade
unter dem Cursor befindlichen Schlüsselworts. •
Es gibt keinen Objektbrowser.
•
Code wird nicht automatisch eingerückt.
•
Fehler werden erst beim Kompilieren erkannt, nicht schon bei der Eingabe.
Hello Console Um das VB.NET-Beispielprogramm aus Abschnitt 1.1 in SharpDevelop zu realisieren, sind zwei (fett hervorgehobene) Änderungen am Code erforderlich. Zum einen bewirkt Imports Microsoft.VisualBasic, dass die Klassen der VB-Bibliothek verwendet werden können, ohne dass jedes Mal Microsoft.VisualBasic vorangestellt werden muss. Zum anderen muss bei jeder Methode aus dieser Bibliothek der Klassenname vorangestellt werden (DateAndTime.Now statt Now, Strings.Left statt Left etc.). Option Strict On Imports System Imports Microsoft.VisualBasic Module Module1 Sub Main() Console.WriteLine("Hello Console!") Console.WriteLine() 'leere Zeile Console.WriteLine("{0}, das heutige Datum ist: {1}!", _ Environment.UserName, DateAndTime.Now.ToLongDateString) End Sub End Module
VERWEIS
2.7 Entwicklungsumgebungen
73
Vielleicht fragen Sie sich, warum der Code nicht ohne Änderungen verwendet werden kann. Der Grund besteht darin, dass sich die Microsoft-Entwicklungsumgebung um so genannte Defaultimporte kümmert und das (undokumentierte) Attribut auswertet. Deswegen muss bei Methoden aus Microsoft.VisualBasic der Klassenname nicht angegeben werden. Diese beiden Besonderheiten sind in Abschnitt 6.2 beschrieben.
Hello Windows Erheblich aufwendiger als bei Konsolenanwendungen ist die Portierung von VB.NETCode von Windows-Anwendungen. Neben den Importen und Referenzen auf Bibliotheken, die von Hand eingefügt werden müssen, unterscheiden sich SharpDevelop und die Microsoft-Entwicklungsumgebung auch darin, wie Windows.Forms-Programme gestartet werden. Microsoft fügt dazu den Code Application.Run(New formname()) ein (siehe Abschnitt 15.2), was bei SharpDevelop nicht der Fall ist. Außerdem fehlt in SharpDevelop der Designer zum Entwurf von Formularen. Natürlich kann diese Arbeit von Hand erledigt werden, indem in den New-Konstruktor der erforderliche Code zum Erzeugen von Buttons etc. eingefügt wird (siehe Abschnitt 15.2.2) – aber wirklich Spaß macht das nicht. Wenn Sie es dennoch versuchen möchten, sollten Ihnen die folgenden Zeilen bei ersten Experimenten helfen. Imports System Imports System.Drawing Imports System.Windows.Forms Public Class MainForm Inherits Form Friend WithEvents Button1 As Button Public Shared Sub Main() Application.Run(New MainForm()) End Sub Public Sub New() MyBase.New() Me.Text = "Hello Windows" Me.ClientSize = New Size(224, 104) ' Button Me.Button1 = New Button() Me.Button1.Location = New Point(112, 72) Me.Button1.Size = New Size(104, 24) Me.Button1.Text = "Ende" Me.Controls.Add(Button1) End Sub
74
2 Das .NET-Universum
HINWEIS
Private Sub Button1_Click(ByVal sender As Object, _ ByVal e As EventArgs) Handles Button1.Click Me.Close() End Sub End Class
SharpDevelop 0.88b hat offensichtlich noch ein Problem beim Kompilieren von Windows-Anwendungen: Zwar kann bei den PROJEKTOPTIONEN angegeben werden, dass der Code als Windows-Programm kompiliert werden soll; SharpDevelop ignoriert diese Option aber und kompiliert das Programm dennoch zu einem Konsolenprogramm. Deswegen wird während der Programmausführung auch ein Konsolenfenster angezeigt, solange das Fenster offen ist.
2.7.3
Ohne Entwicklungsumgebung arbeiten
Wenn Sie die ersten Experimente ganz ohne eine Entwicklungsumgebung machen möchten, sollten Sie als Erstes die Umgebungsvariable PATH so einstellen, dass diese auch auf das Verzeichnis mit den .NET-Tools (Compiler etc.) zeigt. Dazu starten Sie das Modul SYSTEMSTEUERUNG|SYSTEM|ERWEITERT|UMGEBUNGSVARIABLEN und bearbeiten dort PATH. Die .NET-Tools befinden sich im Verzeichnis C:\Windows\Microsoft.NET\Framework\v1.0.3705 (überprüfen Sie die Versionsnummer!).
Abbildung 2.5: Einstellung der PATH-Variablen
2.7 Entwicklungsumgebungen
75
Anschließend starten Sie das Programm START|ZUBEHÖR|EINGABEAUFFORDERUNG und wechseln in das Verzeichnis, in dem sich Ihre VB.NET-Quelltextdatei befindet. (Diese Datei können Sie mit einem beliebigen Editor erstellen.) Zum Kompilieren führen Sie einfach die folgende Anweisung aus (siehe Abbildung 2.6): vbc /target:exe dateiname.vb
Abbildung 2.6: VB.NET-Programm kompilieren und ausführen
Der Compiler erstellt daraus die Datei dateiname.exe, die Sie danach sofort ausprobieren können. Wenn beim Kompilieren Fehler auftreten, werden Sie davon (jeweils mit Zeilennummer) informiert und müssen diese beheben. Im Code müssen Sie die gleichen Details wie bei SharpDevelop beachten (siehe den vorigen Abschnitt). Außerdem müssen Sie beim Kompilieren alle Bibliotheken angeben, die Ihr Programm nutzt (außer mscorlib.dll). Um ein Windows-Programm zu kompilieren, müssen Sie den Compiler wie folgt ausführen. Entscheidend ist, dass Sie bei der Option /target winexe angeben und dass Sie mit der Option /reference alle erforderlichen Bibliotheken angeben. Die Anweisung muss in einer Zeile erfolgen und wurde hier nur aus Platzgründen auf drei Zeilen verteilt.
VERWEIS
vbc /target:winexe /reference:system.dll,system.drawing.dll,system.windows.forms.dll mainform.vb vbc.exe kann durch unzählige weitere Optionen gesteuert werden, die in der Online-Hilfe beschrieben werden. Suchen Sie einfach nach vbc.exe! ms-help://MS.VSCC/MS.MSDNVS.1031/vblr7/html/valrfvbcompileroptionslistedbycategory.htm
3
Von VB6 zu VB.NET
Dieses Kapitel richtet sich an VB6-Programmierer, die gezielt nach Informationen suchen, die die Unterschiede zwischen VB6 und VB.NET betreffen. So viel vorweg: Es hat sich derart viel geändert, dass die Weiterverwendung vorhandenen Codes so gut wie unmöglich ist. Auch der am Ende des Kapitels kurz beschriebene Migrationsassistent kann da nicht wirklich weiterhelfen. VB.NET ist eine neue Programmiersprache mit vielen neuen Funktionen und Vorzügen, aber sie ist ungeeignet, um alten VB6-Code zu warten oder zu migrieren. 3.1 3.2 3.3
Fundamentale Neuerungen in VB.NET Unterschiede zwischen VB6 und VB.NET Der Migrationsassistent
78 79 109
78
3 Von VB6 zu VB.NET
3.1
Fundamentale Neuerungen in VB.NET
VB.NET zeichnet sich durch viele Neuerungen aus, die von grundlegender Natur und nicht einfach Ergänzungen oder Änderungen gegenüber VB6 sind. Dieser Abschnitt fasst die wichtigsten dieser Neuerungen zusammen. (Im nächsten Abschnitt folgen dann Details über die kleinen und großen Inkompatibilitäten zwischen VB6 und VB.NET.) •
Die .NET-Klassenbibliothek: Die wohl größte Neuerung für Visual-Basic-Programmierer betrifft genau genommen gar nicht VB.NET, sondern das darunter liegende Betriebssystem. Durch die .NET-Klassenbibliothek können fast alle Betriebssystemfunktionen über eine einheitliche objektorientierte Schnittstelle genutzt werden. Die Bibliothek enthält Tausende von Klassen und schier unendlich viele Eigenschaften, Methoden, Ereignisse etc. (auf jeden Fall mehr, als Sie jemals nutzen können). Der Zugriff auf die Klassen erfolgt in der Schreibweise System.klasse.subklasse. Die folgende Liste gibt ein paar Beispiele, die lediglich die riesige Bandbreite verdeutlichen sollen, die durch diese Bibliothek abgedeckt wird. System.Data: die neue Datenbankbibliothek ADO.NET System.Drawing: Grafikfunktionen (GDI) System.IO: Zugriff auf Dateien System.OperatingSystems: Informationen über die Version des laufenden Betriebssystem System.Security: Sicherheitsfunktionen, Zugriffsrechte, Kryptographie System.Text.RegularExpression: Bearbeitung von Zeichenketten mit regulären Ausdrücken System.Web: Internet-Programmierung (ASP.NET) System.Web.Services: Internet-Programmierung (Web-Services) System.Windows.Forms: Windows-Programmierung (Fenster, Steuerelemente)
Viele Begriffe, die Sie aus der Werbung für .NET vermutlich schon kennen (ADO.NET, ASP.NET, Web-Services etc.) sind also nichts anderes als Funktionen, die die .NETKlassenbibliothek zur Verfügung stellt und die Sie mit VB.NET nutzen können. •
Namensräume: Wie aus der obigen Liste deutlich wird, bestehen Klassennamen aus mehreren Teilen und können ziemlich lang werden. Zur Organisation der Klassen verwendet Microsoft so genannte Namensräume. Ein Namensraum fasst mehrere Klassen zusammen, deren Name mit denselben Schlüsselwörtern beginnt. Damit Sie bei der Nutzung von Klassen nicht jedes Mal den gesamten Klassennamen angeben müssen, können Sie mit Imports einzelne Namensräume als Default einrichten. Das bewirkt, dass der Compiler bei der Auflösung von Klassennamen automatisch die entsprechenden Namensräume durchsucht.
•
Objektorientierte Programmierung: In VB.NET können Sie Klassen nicht nur anwenden, sondern auch selbst programmieren. VB.NET bietet dazu jetzt weitgehend dieselben Möglichkeiten, die Sie von anderen Programmiersprachen (C++, Java) vielleicht schon kennen: Vererbung, Schnittstelle, Attribute, Delegates (eine Art von Funktionszeigern) etc. Eine ausführliche Erklärung dieser Begriffe folgt in Kapitel 7.
3.2 Unterschiede zwischen VB6 und VB.NET
79
•
Konsolenanwendungen: VB.NET ermöglicht erstmals (wie seit jeher die Programmiersprache C) die Erstellung von so genannten Konsolenanwendungen. Derartige Programme werden in einer Textkonsole (dem ehemaligen DOS-Fenster) ausgeführt. Konsolenanwendungen eignen sich besonders gut für Beispielprogramme, weil es nur einen minimalen Overhead gibt. Ein noch so einfaches Windows-Programm besteht dagegen aus mindestens 50-60 Zeilen Code, die zwar von der Entwicklungsumgebung automatisch erstellt werden, am Anfang aber dennoch für Verwirrung sorgen.
•
Multithreading: Multithreading bedeutet, dass verschiedene Teile eines Programms quasi gleichzeitig ausgeführt werden können. Auch VB.NET unterstützt diese aus anderen Programmiersprachen schon lange bekannte Funktion nun.
•
Entwicklungsumgebung: Zu VB.NET gibt es eine vollkommen neue Entwicklungsumgebung (die einheitlich für alle .NET-Programmiersprachen ist). VB6-Umsteiger brauchen eventuell ein paar Tage, um sich an die Neuerungen zu gewöhnen, aber im Wesentlichen ist die neue Entwicklungsumgebung gelungen und läuft stabil. Einzige Voraussetzung: Ein großer Monitor!
•
Compiler, MSIL: Auch hinter den Kulissen hat sich viel getan. VB.NET wird jetzt immer kompiliert, d.h., es gibt (anders als bei VB6) keinen Interpreter mehr. Das ist nicht immer ein Vorteil – gerade bei der Fehlersuche war der Interpreter von VB6 ausgesprochen praktisch, weil Änderungen im laufenden Programm möglich waren. Neu ist auch die Art, wie Code kompiliert wird: VB6 erzeugte wahlweise so genannten P-Code (der von einem Runtime-Interpreter ausgeführt wird) oder echten Maschinencode. VB.NET erzeugt Code in der MSIL (Microsoft Intermediate Language). Das ist wie PCode ein Zwischencode, der nicht unmittelbar ausgeführt werden kann. Anders als PCode wird MSIL vor der Ausführung aber durch einen Just-in-time-Compiler kompiliert, so dass es keine Geschwindigkeitsnachteile gibt.
3.2
Unterschiede zwischen VB6 und VB.NET
VERWEIS
Dieser Abschnitt fasst die wichtigsten Unterschiede zwischen VB6 und VB.NET zusammen. Dabei handelt es sich aber keineswegs um eine vollständige Referenz. Einige weitere Informationsquellen finden Sie, wenn Sie in der Hilfe nach Änderungen an Visual Basic suchen. http://www.msdn.microsoft.com/library/default.asp?url=/library/en-us/dnvb600/html/vb6tovbdotnet.asp ms-help://MS.VSCC/MS.MSDNVS.1031/vbcn7/html/vacondifferencesbetweenvb6andvb7.htm ms-help://MS.VSCC/MS.MSDNVS.1031/vbcn7/html/vaconProgrammingElementsChangesInVB7.htm ms-help://MS.VSCC/MS.MSDNVS.1031/vbcon/html/ vboriIntroductionToVisualBasic70ForVisualBasicVeterans.htm
Wenn Sie einen noch ausführlicheren und systematischeren Vergleich zwischen VB6 und VB.NET suchen, empfehle ich Ihnen das Buch The .NET Languages von Brian Bischof wärmstens. (Das Buch berücksichtigt außerdem noch C#, was aber kein Nachteil ist – ganz im Gegenteil!)
80
3.2.1
3 Von VB6 zu VB.NET
Variablenverwaltung
Allgemeines VB6
VB.NET
Dim: Dim gilt immer für eine
Dim gilt für den Block, in dem es definiert ist; wenn Dim innerhalb einer Schleife eingesetzt
gesamte Prozedur oder Funktion.
wird, kann die so deklarierte Variable auch nur innerhalb der Schleife verwendet werden. Dim: Mit Dim x, y, z As Long wurden x Dim x, y, z As Long deklariert alle angegebenen und y als Variant-Variable deklariert, Variablen als Long. z dagegen als Long-Variable. Dieses
Verhalten war unlogisch, aber man hat sich mit der Zeit daran gewöhnt. Dim: Eine Initialisierung der
Variablen ist nicht möglich. DefInt, DefDbl etc.: Mit diesen
Kommandos ist es möglich, den Defaultvariablentyp einzustellen (z.B. DefInt A-Z). Option Explicit: Diese Option kann
am Beginn jeder Codedatei eingestellt werden. Sie steuert, ob Variablen vor ihrer Verwendung deklariert werden müssen.
Dim ermöglicht nun auch eine Initialisierung der Variable, z.B. Dim i1 As Integer = 3. DefInt und vergleichbare Kommandos gibt es nicht mehr. Als Defaultvariablentyp gilt Object (der Nachfolger des Variant-Typs).
Die Option kann weiterhin im Programmcode eingestellt werden. Dabei muss nun zusätzlich das Schlüsselwort On oder Off verwendet werden, um die gewünschte Einstellung zu verdeutlichen. Während die Option-Einstellungen im Programmcode nur die jeweilige Codedatei gelten, gibt es nun auch Defaulteinstellungen, die für das gesamte Projekt gelten. Diese können mit PROJEKT|EIGENSCHAFTEN eingestellt werden. Per Default gilt: Option Explicit On
Typenkonvertierung: VB6 führt generell eine automatische Typenkonvertierung durch, etwa wenn Sie integervar = "3" ausführen. Dieser Automatismus ist bequem, verbirgt aber manchmal Programmierfehler.
Per Default ist alles beim Alten geblieben. Es gibt aber eine neue Option Option Strict. Die Einstellung erfolgt wie bei Option Explicit (siehe oben). Per Default gilt: Option Strict Off
3.2 Unterschiede zwischen VB6 und VB.NET
VB6
81
VB.NET Wenn die Option aber aktiviert wird, führt VB.NET nur mehr solche Umwandlungen automatisch durch, die unbedenklich sind und mit Sicherheit ohne Datenverlust durchgeführt werden können (etwa von Long zu Double). Alle anderen Umwandlungen werden bereits bei der Entwicklung des Programms mit einem Syntaxfehler quittiert. Sie sind dann gezwungen, eine geeignete Funktion zur Umwandlung selbst einzufügen. Eine Nebenwirkung von Option Explicit On besteht darin, dass alle Variablen explizit mit Typ deklariert werden müssen. Der Vorteil besteht darin, dass Sie auf diese Weise eine (nicht unerhebliche) Fehlerquelle ausschließen.
Set: Mit diesem Kommando werden
Objektzuweisungen durchgeführt (Set obj = anderesobjekt).
Objektzuweisungen erfolgen jetzt einfach durch den Operator =. Das Kommando Set ist nicht mehr erforderlich und existiert deswegen nicht mehr. (Wenn Sie es versehentlich eingeben, wird es von der Entwicklungsumgebung sofort wieder entfernt.)
Variablentypen VB6
VB.NET
Variablen versus Objekte: VB6 unterscheidet streng zwischen gewöhnlichen Variablen und Objekten. Objekte zeichnen sich dadurch aus, dass es hierfür Methoden und Eigenschaften gibt.
Bei VB.NET gilt jede Variable als Objekt. Selbst auf Variablen für elementare Datentypen (z.B. Integer, Date, String) können zahlreiche Methoden angewandt werden. Beispielsweise liefert intvar.MaxValue den größten Wert, der in dieser Variable gespeichert werden darf. strvar.Length liefert die Anzahl der gespeicherten Zeichen.
Variant-Variablentyp: Dieser
Den Datentyp Variant gibt es nicht mehr. Der Defaultdatentyp heißt nun Object und hat teilweise ähnliche Eigenschaften wie Variant.
Variablentyp gilt als Defaultvariablentyp.
82
3 Von VB6 zu VB.NET
VB6
VB.NET
Integer-Variablentypen:
Die Bedeutung von Integer und Long hat sich geändert:
Integer-Zahlen sind 2 Byte lang. Long-Zahlen sind 4 Byte lang.
Integer-Zahlen sind jetzt 4 Byte lang. Long-Zahlen sind jetzt 8 Byte lang.
Als Ersatz für 2-Byte-Integerzahlen gibt es den neuen Datentyp Short (2 Byte, für Werte zwischen -32.768 bis 32.767). Single- und Double-Variablentypen:
Single und Double kennen nun auch die
Bei ungültigen Berechnungen, z.B. 1/0.0, Sqr(-1) und Log(-1), treten Fehler auf.
Sonderwerte plus und minus unendlich sowie not a number. Ungültige Fließkommaberechnungen führen normalerweise nicht mehr zu einem Fehler, sondern resultieren in den erwähnten Sonderwerten. Diese Werte können Sie mit den Methoden Double.IsNaN(x) oder Single.IsNegativeInfinity(s) feststellen. Gerade bei der Entwicklung mathematischer Anwendungen ist dieses Verhalten sehr gewöhnungsbedürftig, weil offensichtliche Fehler oft verborgen bleiben.
Decimal- und Currency-Datentypen: Currency: Festkommazahlen, 15
Stellen vor, 4 nach dem Komma. Platzbedarf 4 Byte. Kennzeichnung mit @.
Den Datentyp Currency gibt es nicht mehr. Dafür ist Decimal nun ein eigenständiger Datentyp mit ansonsten unveränderten Eigenschaften. Mit @ gekennzeichnete Variablen gelten nun automatisch als Decimal-Variablen.
Decimal: Festkommazahlen mit 28 Stellen, als Variant-Untertyp
realisiert. Platzbedarf 12 Byte. String-Datentyp: Mit Dim s As String * 10 können Zeichenkettenvariablen
konstanter Länge definiert werden.
Zeichenkettenvariablen mit vorgegebener Länge gibt es nicht mehr. Für diesen Zweck können Char-Felder eingesetzt werden. Darüber hinaus gibt es zahlreiche Änderungen, die den Umgang mit Zeichenketten betreffen (siehe Abschnitt 3.2.3).
Date-Datentyp: Die interne
Darstellung erfolgte durch einen Double-Wert (8 Byte).
Die interne Darstellung erfolgt nun durch einen Long-Wert (ebenfalls 8 Byte). Eine direkte Addition von Daten und Zeiten mit + ist nicht mehr möglich. Dafür gibt es zahlreiche neue Methoden zur Bearbeitung derartiger Variablen (z.B. datevar.Add).
3.2 Unterschiede zwischen VB6 und VB.NET
VB6
83
VB.NET Zur Konvertierung zwischen Date-Variablen und dem VB6-kompatiblen Fließkommaformat gibt es die neuen Methoden To- und FormOADate.
Konstanten True und False: True hat den Wert -1, False 0.
Hier hat sich nichts geändert. (In der ersten BetaVersion von VB.NET galt True=1, aber in der zweiten wurde diese Änderung wieder rückgängig gemacht.)
Felder und Aufzählungen VB6
VB.NET
Option Base: Diese Anweisung bestimmt, ob ein mit Dim x(3) deklariertes Feld von x(0) bis x(3)
Die Anweisung Option Base gibt es nicht mehr. Die Indizes eines mit Dim x(n) deklarierten Felds reichen immer von 0 bis n.
reicht (Defaulteinstellung), oder von x(1) bis x(3) (Option Base 1). Dim x(-5 To 7): In VB6 besteht die
VB.NET bietet diese Möglichkeit nicht mehr. Der Möglichkeit, bei der Feldindex reicht immer von 0 bis n. Dimensionierung von Feldern sowohl den unteren als auch den oberen Grenzwert des Indexbereichs anzugeben. Datenfelder (Array): In VariantVariablen können so genannte Datenfelder gespeichert werden, die mit Array sehr komfortabel initialisiert werden können.
Datenfelder und das Schlüsselwort Array sind zusammen mit dem Variant-Datentyp abgeschafft worden. Um mehrere Elemente eines Felds gleichzeitig zu initialisieren, ist jetzt eine neue Schreibweise möglich: Dim myarray As Integer = {1, 2, 3}
Aufzählungen: In VB6 können Sie Aufzählungen mit den beiden Klassen Collection (VB-intern) und Dictionary (Scripting-RuntimeBibliothek) verwalten.
Unter VB.NET stehen eine VB6-kompatible Collection-Klasse noch immer zur Verfügung (Microsoft.VisualBasic.Collection). Darüber hinaus stellt die .NET-Bibliothek aber gleich eine ganze Sammlung neuer Collection-Klassen zur Verfügung, die viel leistungsfähiger sind als die aus VB6 vertrauten Aufzählungen. Die neuen Aufzählklassen werden ausführlich in Kapitel 9 vorgestellt. Eine syntaxkompatible Entsprechung von Dictionary gibt es allerdings nicht.
84
3 Von VB6 zu VB.NET
Eigene Datenstrukturen VB6
VB.NET
Type: Mit Type – End Type können
Type wird nicht mehr unterstützt. Stattdessen gibt es nun Structure – End Structure zur Bildung von
sehr einfach Strukturen aus elementaren Datentypen zusammengesetzt werden.
3.2.2
eigenen Datenstrukturen. Strukturen bieten zwar viel mehr Möglichkeiten, sind aber inkompatibel zu Type.
Sprachsyntax
Im Bereich der objektorientierten Programmierung gibt es zahllose neue Schlüsselwörter, die unter anderem erstmals eine echte Vererbung bei der Definition von Klassen ermöglichen. Die Beschreibung dieser Merkmale füllt das gesamte Kapitel 7. Daher beschränkt sich die folgende Tabelle auf die prozeduralen Sprachelemente. VB6
VB.NET
While-Wend-Schleifen: Derartige Schleifen können mit While bedingung – Wend formuliert werden.
Die Syntax lautet nun Do While bedingung – Loop.
Gleichnamige Prozeduren: Es besteht keine Möglichkeit, zwei gleichnamige Prozeduren zu deklarieren.
VB.NET erlaubt die Deklaration gleichnamiger Prozeduren, wenn diese anhand der Parameterliste eindeutig identifizierbar sind.
Aufruf von Prozeduren und Methoden: Parameter von Prozeduren und Methoden ohne Rückgabewert müssen nicht in Klammern gesetzt werden.
Parameter von Prozeduren und Methoden müssen nun in Klammern gestellt werden. (Die Entwicklungsumgebung fügt die Klammern selbstständig ein, wenn Sie es vergessen.)
Parameterübergabe: Per Default werden Parameter mit ByRef an Prozeduren übergeben, können also verändert werden.
Per Default erfolgt die Parameterübergabe nun mit ByVal. Änderungen der Parameter in der Prozedur haben daher keine Auswirkung auf Variablen außerhalb der Prozedur.
Optionale Parameter: Bei optionalen Bei der Deklaration optionaler Parameter muss Variant-Parametern können Sie mit nun ein Defaultwert angegeben werden. IsMissing IsMissing testen, ob ein Wert steht nicht mehr zur Verfügung. übergeben wurden. Die Angabe von Defaultwerten ist optional.
3.2 Unterschiede zwischen VB6 und VB.NET
85
VB6
VB.NET
Funktionsergebnis zurückgeben: Das Resultat einer Funktion wird durch die Zuweisung funktionsname=ergebnis bestimmt.
Die aus VB6 vertraute Syntax ist in VB.NET weiterhin möglich. Darüber hinaus kennt VB.NET nun aber das neue Schlüsselwort Return, mit dem das Funktionsergebnis angegeben werden kann. (Durch Return wird die Funktion gleichzeitig beendet. Return kann auch dazu verwendet werden, ein Unterprogramm zu beenden. Es hat dann dieselbe Wirkung wie Exit Sub.)
Statische Prozeduren: Wenn der Deklaration einer Prozedur das Schlüsselwort Static vorangestellt wird, gelten alle darin enthaltenen Variablen als statisch (behalten also ihren Wert bei mehreren Aufrufen der Prozedur).
In VB.NET kann Static zwar weiterhin zur Deklaration einzelner Variablen verwendet werden, nicht aber zur Deklaration ganzer Prozeduren.
And- und Or-Operatoren: Bei a Or b bzw. a And b wird b auch dann ausgewertet, wenn a wahr ist (Or) bzw. falsch ist (And). Wenn es darum geht, einen Wahrheitswert zu ermitteln, ist das sinnlos, weil die Auswertung von b in diesem Fall nichts mehr am Ergebnis ändern kann.
And und Or verhalten sich aus
Imp- und Eqv-Operatoren: Mit den Operatoren Imp und Eqv können die Implikation (wenn a wahr ist, muss auch b wahr sein) und die Äquivalenz von Wahrheitswerten getestet werden.
Die Operatoren Imp und Eqv stehen nur noch als Methoden der Bibliothek Microsoft.VisualBasic.Compatibility.dll zur Verfügung. Da die Dokumentation von der Verwendung dieser Bibliothek abrät, sollten Sie folgende Ersatzkonstruktionen verwenden:
Kompatibilitätsgründen weiterhin so wie bisher. Es gibt aber immerhin die neuen Operatoren AndAlso bzw. OrElse, die sich so verhalten, wie man das von logischen Operatoren aus allen anderen Programmiersprachen kennt.
a Imp b entspricht (Not a) Or b a Eqv b entspricht a = b
Set-Operator: Der Set-Operator wird Set gibt es nicht mehr, Objekte werden nun mit = zur Zuweisung von Objekten zugewiesen. verwendet (z.B. Set obj1 = obj2). Operatoren +=, -= etc.: VB6 kennt keine derartigen Operatoren.
In VB.NET können Sie Variablen durch x+=1, y-=2, z*=wert, s+="abc" bearbeiten. Das verringert den Schreibaufwand (und die Gefahr von Tippfehlern). Die von C bekannten Operatoren ++, -- etc. kennt VB.NET weiterhin nicht.
86
3.2.3
3 Von VB6 zu VB.NET
Bearbeitung von Zahlen und Zeichenketten
Arithmetische Funktionen VB6
VB.NET
Arithmetische Funktionen wie Sin, Cos etc. stehen ohne weiteres zur Verfügung.
Arithmetische Funktionen sind Teil der Systems.Math-Klasse. Statt Sin(x) müssen Sie jetzt Math.Sin(x) angeben. Außerdem haben einige Funktionen einen anderen Namen erhalten.
Bearbeitung von Daten und Zeiten VB6
VB.NET
Addition von Zeiten: Wegen der internen Darstellung durch DoubleWerte können Zeiten einfach mit dem Operator + addiert werden.
Die interne Darstellung von Date-Variablen hat sich geändert. Eine direkte Addition von Zeiten ist nicht mehr möglich, dafür gibt es aber diverse Add-Methoden. Ansonsten gibt es aber kaum Kompatibilitätsprobleme, d.h., alle aus VB6 vertrauten Funktionen funktionieren weiterhin. Darüber hinaus stehen zahllose neue Funktionen zur Verfügung.
Bearbeitung von Zeichenketten Obwohl sich der Umgang mit Zeichenketten auf den ersten Blick kaum geändert hat, gibt es fundamentale Änderungen unter der Oberfläche. VB6
VB.NET
Zeichenketten sind veränderlich. Das bedeutet, dass eine Zeichenkette verändert werden kann, ohne dass sich ihr Ort im Speicher ändert.
Zeichenketten sind unveränderlich. Bei jeder Änderung (s = s + "abc") wird eine neue Zeichenkette erzeugt und an einem anderen Ort im Speicher abgelegt (auch dann, wenn sich durch die Veränderung die Länge der Zeichenkette nicht ändert).
3.2 Unterschiede zwischen VB6 und VB.NET
87
VB6
VB.NET
Funktionen zur Bearbeitung von Zeichenketten: VB stellt eine Fülle von Funktionen bzw. Kommandos zur Manipulation von Zeichenketten zur Verfügung (Left, Mid, Len etc.).
Diese Funktionen stehen weiterhin zur Verfügung. Daneben gibt es aber eine Menge weiterer Funktionen, die als Eigenschaften oder Methoden von String-Variablen oder auch als eigenständige Funktionen verschiedener Namensräume genutzt werden können (System.String, System.Text.RegularExpression etc.). Bei der Verwendung dieser Funktionen ist zu beachten, dass das erste Zeichen mit dem Index 0 angesprochen wird, während bei herkömmliche VB-Funktionen der Index 1 gilt.
Variant- versus String-Funktionen: VB stellt fast alle Funktionen zur Bearbeitung von Zeichenketten doppelt zur Verfügung: Beispielsweise dient Left zur Bearbeitung von Variant-Variablen, Left$ zur Bearbeitung von String-Variablen.
Alle Zeichenkettenfunktionen, deren Name mit $ endet, wurden eliminiert. Verwenden Sie stattdessen die gleichnamigen Funktionen ohne $.
Funktion String: Diese Funktion ermöglicht die Vervielfältigung von Zeichenketten. String("ab", 3) liefert "ababab".
Diese Funktion gibt es nicht mehr. Wenn Sie nur ein einzelnes Zeichen (und nicht eine Zeichenkette) vervielfältigen möchten, können Sie New String oder StrDub verwenden: s = New String("x"c, 3) s = StrDup(3, "x")
Funktionen LenB, LeftB etc.: Diese Kommandos ermöglichen ab VB4 eine byteorientierte Bearbeitung von Zeichenketten.
3.2.4
Diese Funktionen gibt es ebenfalls nicht mehr. Sie können aber Zeichenketten mit den Methoden aus System.Text.Encoding in ein Byte-Feld umwandeln (siehe Abschnitt 8.2.5).
Windows-Anwendungen und Steuerelemente
Windows-Fenster und die meisten Steuerelemente basieren auf der neuen Windows.FormsBibliothek. Die Konsequenz lautet kurz gefasst, dass zwar vieles so aussieht wie früher, aber nur wenig noch so funktioniert. Neue Steuerelement-, Eigenschafts- und Methodennamen erschweren den Umstieg zusätzlich. Die folgende Beschreibung von Änderungen ist keinesfalls vollständig, sondern beschränkt sich auf die auffälligsten Details (bzw. auf Punkte, die am häufigsten Umstellungsschwierigkeiten verursachen).
88
3 Von VB6 zu VB.NET
Formulare (Fenster) VB6
VB.NET
Show vbModal: Mit dieser Methode
Statt Show vbModal muss nun die Methode ShowDialog verwendet werden, die dieselbe Wirkung hat, aber einen Ergebniswert zurückgibt. Dieser Wert kann durch die Eigenschaft DialogResult eingestellt werden.
kann ein Formular modal angezeigt werden, d.h., der Rest des Programms ist blockiert, bis dieses Formular wieder geschlossen wird. Programmende: Das Programm endet automatisch, wenn das letzte offene Fenster geschlossen wird.
Das Programm endet automatisch, wenn das Startfenster geschlossen wird. (Alle anderen offenen Fenster werden dann ebenfalls geschlossen. Tipps, wie Sie ein anderes Verhalten erzielen können, finden Sie in Abschnitt 15.3.
Schriftarten: VB6 unterstützt alle unter Windows zur Auswahl stehenden Schriftarten.
VB.NET (genau genommen das .NET-Grafiksystem GDI+) unterstützt nur noch TrueTypebzw. OpenType-Schriftarten. Die VB6Defaultschrift MS Sans Serif wird nicht mehr unterstützt. Verwenden Sie stattdessen Arial oder die namensähnliche Schrift Microsoft Sans Serif. (Details zum Umgang mit Schriften finden Sie in Abschnitt 16.3.)
Standardsteuerelemente In VB.NET gibt es die Unterscheidung zwischen Standard- und Zusatzsteuerelementen nicht mehr. Alle Steuerelemente sind gleichberechtigt. In diesem Abschnitt wird aber dennoch die aus VB6 bekannte Zweiteilung beibehalten, um den Abschnitt übersichtlicher zu halten. (Die weiteren Kapitel zu diesem Thema halten sich dann an die neue Kategorisierung der Steuerelemente.) VB6
VB.NET
CommandButton
Der CommandButton heißt nun einfach Button.
DriveListBox, DirListBox und FileListBox: Diese Steuerelemente
Die Steuerelemente werden unter .NET nicht mehr offiziell unterstützt, stehen aber als Teil der Bibliothek microsoft.visualbasic.compatibility weiterhin zur Verfügung. Die Dokumentation rät allerdings davon ab, diese Bibliothek in neuen Projekten zu verwenden.
zeigen Listenfelder mit Laufwerken, Verzeichnissen und Dateien an.
3.2 Unterschiede zwischen VB6 und VB.NET
VB6
89
VB.NET
Frame: Das Steuerelement fasst
Statt des Frame-Steuerelements stehen zwei neue mehrere andere Steuerelemente (z.B. Steuerelemente zur Auswahl: GroupBox sieht aus Optionsfelder) zu einer Gruppe und funktioniert wie Frame. Panel kann im zusammen. Gegensatz zu Frame nicht beschriftet werden, kann dafür aber mit Scroll-Balken ausgestattet werden, um einen beliebig großen Innenbereich zu verwalten. Label: Der angezeigte Text wird über Der Text wird nun (wie bei allen anderen die (Default-)Eigenschaft Caption Steuerelementen) durch die Eigenschaft Text
gesteuert.
eingestellt.
ListBox, ComboBox: Die beiden
Der Zugang auf die Listenelemente erfolgt nun über die Items-Eigenschaft, die auf ein Objekt der Klasse ComboBox.- bzw. ListBox.ObjectCollection verweist. Listeneinträge werden mit Items.Add(...) oder Insert(...) eingefügt.
Steuerelemente dienen zur Anzeige von (ausklappbaren) Listen. Als Listeneinträge sind nur Zeichenketten zulässig, die über List(n) gelesen bzw. verändert werden können. Optional kann zu jedem Listenelement über die Aufzähleigenschaft ItemData(n) ein Variant-Objekt mit Zusatzinformationen gespeichert werden.
Image, PictureBox: Das Image-
Steuerelement dient zur Anzeige unveränderlicher Bitmaps. Im PictureBox-Steuerelement können dagegen eigene Grafiken dargestellt werden. Das PictureBox-Steuerelement kann auch als Container für andere Steuerelemente verwendet werden.
Als Listeneinträge sind beliebige Objekte erlaubt, wobei die Textrepräsentation (ToString-Methode) angezeigt wird. Da zusammen mit diesen Objekten beliebige Zusatzinformationen gespeichert werden können, ist DataItem nicht mehr erforderlich und wird nicht mehr unterstützt. Die Portierung von VB6-Code mit DataItem erfordert aber eine grundlegende Umstellung des Codes (Deklaration einer eigenen Klasse zur Speicherung der Zusatzinformationen). Es gibt nur noch das PictureBox-Steuerelement, das sowohl zur Grafikprogrammierung als auch zur Anzeige von Bitmaps verwendet werden kann. Die Grafikprogrammierung hat sich aber grundlegend gegenüber VB6 geändert. Einerseits gibt es zahllose neue Methoden, andererseits wird die praktische AutoRedraw-Eigenschaft nicht mehr unterstützt. Das PictureBox-Steuerelement ist nicht mehr als Container verwendbar. (Stattdessen können Sie das Panel- oder das GroupBox-Steuerelement verwenden.)
90
3 Von VB6 zu VB.NET
VB6
VB.NET
Menüs: Zur Gestaltung eigener Menüs bietet VB6 einen vollkommen veralteten und nicht intuitiven Editor an.
Die Verwaltung von Menüs hat sich intern wie extern stark verändert. Intern dienen die Klassen MainMenu, ContextMenu und MenuItem zur Verwaltung der Menüeinträge. Extern steht zur Gestaltung von Menüs nun endlich ein zeitgemäßer Editor zur Verfügung. Ziemlich ärgerlich bei der Programmierung ist der Umstand, dass die MenuItem-Klasse weder eine Name- noch eine Tag-Eigenschaft kennt. Eine große Enttäuschung ist auch das optische Erscheinungsbild: Menüs mit kleinen Icons, wie sie z.B. im Office-Paket seit Jahren üblich sind, können weiterhin nur mit großem Programmieraufwand realisiert werden.
OptionButton: Das Defaultereignis
(dessen Ereignisprozedur bei einem Doppelklick in der Entwicklungsumgebung eingefügt wird), lautet Click. Es wird bei einem Mausklick aufgerufen, also immer dann, wenn diese Option aktiviert wird. Timer: Das Steuerelement löst in
regelmäßigen Abständen ein Ereignis aus.
Der OptionButton heißt nun RadioButton. Das Defaultereignis lautet nun CheckedChanged und wird bei jeder Veränderung aufgerufen (also auch dann, wenn die Option deaktiviert wird, weil eine andere Option ausgewählt wurde!).
In VB.NET gibt es mehrere Möglichkeiten, Ereignisse periodisch auszulösen. Das TimerSteuerelement unterscheidet sich von der VB6Version vor allem dadurch, dass Interval=0 den Ereignisaufruf nicht stoppt, sondern als Interval=1 interpretiert (d.h. ein Ereignisaufruf pro Millisekunde!). Um die Timer-Ereignisse zu stoppen, muss Enabled auf False gesetzt werden.
Zusatzsteuerelemente VB6
VB.NET
Common Controls: Die drei CommonControls-Bibliotheken
Viele dieser Steuerelemente gibt es weiterhin, sie sind nun aber wie alle anderen Steuerelemente Teil der Windows.Forms-Bibliothek. Viele Steuerelemente haben neue Namen bekommen.
enthalten eine ganze Reihe von Zusatzsteuerelementen.
3.2 Unterschiede zwischen VB6 und VB.NET
91
VB6
VB.NET
CommonDialog: Mit diesem
Das CommonDialog-Steuerelement gibt es in seiner bisherigen Form nicht mehr. Dafür gibt es eine ganze Reiher neuer Steuerelemente (z.B. OpenFileDialog, FontDialog etc.), die dieselben Aufgaben übernehmen. Allerdings haben sich die Namen vieler Eigenschaften und Methoden geändert, so dass alter Code nicht mehr verwendet werden kann.
Steuerelement können in VB6 Auswahldialoge (für Dateien, Schriftarten, Drucker etc.) dargestellt werden.
DataGrid und MS[H]FlexGrid: Diese
Steuerelemente ermöglichen die Darstellung von Tabellen, wobei die Daten wahlweise von einer Datenbank stammen können.
Das vollkommen veränderte DataGridSteuerelement ersetzt gleichermaßen das alte DataGrid sowie MS[H]FlexGrid. Wie alle datenbanktauglichen .NET-Steuerelemente kann DataGrid ausschließlich mit ADO.NETDatenquellen umgehen, nicht aber mit ADO-, RDO- oder DAO-Datenquellen
DataList und DataCombo: Diese
In .NET sind bereits die Basissteuerelemente Steuerelemente ermöglichen die datenbanktauglich, d.h., die neue ListBox ersetzt Darstellung von Listen, die aus einer DataList und ComboBox ersetzt DataCombo. Datenbank stammen dürfen. DataReport: Dieses vollkommen
In .NET ist DataReport wieder verschwunden, unausgereifte Steuerelement war der stattdessen vertraut Microsoft nun wieder auf die Versuch Microsofts, ein eigenes Funktionen von Crystal Reports (Steuerelement Steuerelement für DatenbankCrystalReportViewer). berichte zu entwickeln. ImageList: Das Steuerelement dient
zur Aufbewahrung von mehreren gleich großen Bitmaps, die dann in anderen Steuerelementen (z.B. ListView, TreeView) angezeigt werden.
RichTextBox: Mit der Methode SelPrint kann der zuvor markierte
Text ausgedruckt werden.
Das Steuerelement funktioniert im Prinzip wie bisher, allerdings ist es nicht mehr möglich, mit Namen auf die Bitmaps zuzugreifen (d.h., die aus VB6 vertraute Key-Eigenschaft ist verschwunden). Stattdessen müssen Indexnummern verwendet werden (die sich aber ändern, wenn eine Bitmap aus dem Steuerelement entfernt wird). Der einzige Kommentar, der mir dazu einfällt: idiotisch! Das Steuerelement unterstützt keinen Ausdruck mehr. Nahezu alle Eigenschaften haben neue Namen.
92
3 Von VB6 zu VB.NET
VB6
VB.NET
StatusBar: Mit dem Steuerelement
Das Steuerelement existiert weiterhin, verwendet aber ein vollkommen neues Objektmodell. Die automatische Darstellung von Zeit, Datum, CapsLock und Überschreibmodus ist dem .NETUmstieg leider zum Opfer gefallen. (Der CapsLock-Status kann überhaupt nur durch den Aufruf der API-Funktion GetKeyState ermittelt werden. Hier gibt es noch Lücken im .NETFramework.)
kann eine Statuszeile angezeigt werden. Innerhalb dieser Zeile können unter anderem die Uhrzeit, das Datum, der CapsLock-Zustand und der Überschreibmodus angezeigt werden, wobei sich das Steuerelement selbst um die Aktualisierung dieser Werte kümmert. TabStrip, SSTab: Mit beiden
Steuerelementen können mehrblättrige Dialoge gebildet werden. (Das MultiPageSteuerelement der MS-FormsBibliothek bietet eine dritte Möglichkeit, die unter VB6 aber nur mit Einschränkungen funktioniert.) ToolBar: Mit dem Steuerelement
kann eine Symbolleiste definiert werden.
Das neue TabControl-Steuerelement vereint alle positiven Eigenschaften der drei nebenstehend erwähnten Steuerelemente: Es ist einfach zu bedienen, der Innenbereich jeder Dialogseite kann mit Scroll-Balken ausgestattet werden etc. Dass es damit aber auch zu allen drei genannten Steuerelementen inkompatibel ist, versteht sich gewissermaßen von selbst. Das Steuerelement existiert weiterhin, verwendet aber ein vollkommen neues Objektmodell. Im Vergleich zu VB6 gibt es als einziges neues Merkmal so genannte Dropdown-Buttons, nach deren Anklicken ein Menü erscheint. Dafür gibt es aber leider gleich mehrere Einschränkungen: Den einzelnen Buttons fehlt die Key-Eigenschaft, die bei der Identifizierung in der ClickEreignisprozedur hilfreich war. (Als Ersatz können Sie die Eigenschaft Tag verwenden.) Es gibt keine Möglichkeit mehr, andere Steuerelemente in die Symbolleiste einzufügen. Der Anwender hat im laufenden Programm keine Möglichkeit, die Symbolleiste zu verändern (d.h., es gibt keinen Ersatz für die VB6-Eigenschaft AllowCustomize).
Bei einer Reihe von Steuerelementen haben sich nicht nur viele Eigenschaften und Methoden geändert, sondern auch der Name des Steuerelements. Die folgende Tabelle hilft bei der Suche.
3.2 Unterschiede zwischen VB6 und VB.NET
VB6
VB.NET
DTPicker
DateTimePicker
MonthView
MonthCalender
Slider
TrackBar
UpDown
NumericUpDown
93
Nicht mehr bzw. nur mehr eingeschränkt unterstützte Steuerelemente Grundsätzlich können die meisten herkömmlichen COM- bzw. ActiveX-Steuerelemente auch in VB.NET-Programmen verwendet werden. Insofern stehen also fast alle Steuerelemente weiterhin zur Verfügung. Allerdings ist die Verwendung von ActiveX-Komponenten in VB.NET mit Nachteilen verbunden: es gibt Sicherheitseinschränkungen, die Effizienz ist geringer als bei reinen .NET-Programmen und bisweilen treten Kompatibilitätsprobleme zwischen ActiveX und .NET auf. Aus diesen Gründen werden mit VB.NET fast keine COM- bzw. ActiveX-Steuerelemente mitgeliefert. Die einzigen Ausnahmen sind die Steuerelemente MSChart, Masked Edit und DBGrid (Details siehe in der folgenden Tabelle). Wenn Sie die anderen aus VB6 vertrauten Steuerelemente unter VB.NET nutzen möchten, muss entweder VB6 am selben Rechner installiert sein oder Sie müssen alle erforderlichen *.ocx- und *.dll-Dateien in das WindowsSystemverzeichnis kopieren (was aber fehleranfällig ist). Die ActiveX-Zusatzsteuerelemente müssen zur Verwendung in der Entwicklungsumgebung lizenziert sein. Das ist automatisch der Fall, wenn VB6 am selben Rechner installiert ist. Andernfalls muss die Datei extras\vb6 controls\vb6controls.reg ausgeführt werden. Diese Datei befindet sich auf der VS.NET-CD und enthält die Lizenzschlüssel für die Registrierdatenbank. Die folgende Liste zählt unter VB6 gebräuchliche Steuerelemente auf, zu denen es keine .NET-kompatible Version gibt. VB6
VB.NET
Masked Edit: Das Steuerelement ist eine Variante zur TextBox, mit der
Das Steuerelement steht in unveränderter Form als ActiveX-Steuerelement zur Verfügung einzelne Zeichen der Texteingabe (Name Microsoft Masked Edit Control, Datei besonders formatiert, verifiziert oder Windows\System32\msmask32.ocx). Bevor Sie das maskiert werden können. Steuerelement verwenden können, müssen Sie es selbst in die Toolbox einfügen. MSChart: Mit dem Steuerelement
Das Steuerelement steht ebenfalls als ActiveXkönnen Geschäftsdiagramme erstellt Steuerelement zur Verfügung (Microsoft Chart werden. Control, mschart20.ocx).
94
3 Von VB6 zu VB.NET
VB6
VB.NET
DBGrid: Dieses Steuerelement
Merkwürdigerweise steht auch dieses sehr alte Steuerelement als ActiveX-Steuerelement zur Verfügung. Es wird allerdings nicht automatisch installiert. Falls Sie das Steuerelement unter VB.NET verwenden möchten, kopieren Sie die *.ocx- und *.dll-Dateien aus dem Verzeichnis extras\vb6 controls der VS.NET-CD in das Windows-Systemverzeichnis. Anschließend fügen Sie das Steuerelement in die Toolbox ein.
stammt aus VB5 und dient zur tabellarischen Darstellung von Datenbankdaten. (Es wurde in VB6 durch DataGrid abgelöst.)
Datenbanksteuerelemente: VB6 kannte eine ganze Reihe spezieller Datenbanksteuerelemente für ADO.
Bei .NET gibt es keine Unterscheidung mehr zwischen gewöhnlichen und Datenbanksteuerelementen. Alle Steuerelemente, bei denen dies sinnvoll ist, sind automatisch datenbanktauglich (Eigenschaft DataBinding). Allerdings wird als Datenquelle nur ADO.NET unterstützt. Außerdem sind einige aus VB6 bekannte Datenbanksteuerelemente ersatzlos gestrichen worden (AdoDC, DataRepeater, DataReport).
Internet-Steuerelemente: Die Steuerelemente Inet, WinSock, MAPIMessage und MAPISession helfen bei der Herstellung von HTTP/TCP/UDP-Verbindungen und beim Versenden von E-Mails.
Diese Steuerelemente gibt es nicht mehr. Vergleichbare Funktionen finden Sie aber in den Namensräumen der .NET-Bibliothek (z.B. System.Net and System.Net.Sockets, System.Web.Mail).
WebBrowser: Dieses Steuerelement
Das ActiveX-Steuerelement kann wie alle anderen ActiveX-Steuerelemente weiterhin verwendet werden. Es gibt aber keine .NET-konforme Möglichkeit, den Internet Explorer zu steuern.
stammt aus einer Bibliothek, die zusammen mit dem Internet Explorer installiert wird. Das Steuerelement konnte in VB6 dazu verwendet werden, um HTMLDokumente anzuzeigen. MS-Forms-Steuerelemente: Diese Steuerelemente gehören ebenfalls nicht direkt zu VB6, sondern werden als Bestandteil des Internet Explorers bzw. des Office-Pakets mitgeliefert. Sie können unter VB6 mit Einschränkungen verwendet werden.
Eine Verwendung unter .NET ist ebenfalls nur mit Einschränkungen möglich, weil die MS-FormsSteuerelemente auf ActiveX-Technologie basieren und zum Teil Kompatibilitätsprobleme auftreten. Die Steuerelemente bieten aber ohnedies kaum Funktionen, die die normalen .NETSteuerelemente nicht ebenfalls bieten.
3.2 Unterschiede zwischen VB6 und VB.NET
95
VB6
VB.NET
Windowless-Steuerelemente: Das sind besonders ressourcensparende Versionen der Standardsteuerelemente.
.NET unterstützt offensichtlich keine WindowlessSteuerelemente. (Auf jeden Fall gibt taucht der Begriff windowless in der gesamten .NETFramework-Dokumentation nur ein einziges Mal auf und da in einem anderen Kontext.)
Sonstige Steuerelemente: Adodc,
Diese Steuerelemente stehen unter VB.NET nicht mehr zur Verfügung. Zum Teil ist das ein echter Verlust an Funktionen (z.B. bei MSComm), zum Teil gibt es aber äquivalente Funktionen in den bereits erwähnten .NET-Steuerelementen oder aber in verschiedenen .NET-Bibliotheken.
Animation, CoolBar, DataRepeater, FlatScrollBar, Image, ImageCombo, Line, MMControl, MSComm, OLEFeld, PictureClip, Shape und SysInfo
Unsichtbare Steuerelemente In VB6 gibt es Steuerelemente, die eigentlich gar keine richtigen Steuerelemente sind. Derartige Steuerelemente können nicht direkt in ein Formular eingefügt werden und bleiben daher auch bei der Programmausführung im Formular unsichtbar. (Manche dieser Steuerelemente zeigen sich bei ihrer Anwendung immerhin durch eigene Dialogboxen.) Beispiele für derartige Steuerelemente sind das CommonDialog-Steuerelement zur Darstellung von Auswahldialogen, das ImageList-Steuerelement zur Verwaltung von Bitmap-Dateien oder das WinSock-Steuerelement zur Herstellung einer TCP- oder UDP-Netzwerkverbindung zwischen zwei Programmen etc. In VB.NET scheint es derartige Steuerelemente ebenfalls zu geben. So finden Sie in der Toolbox Steuerelemente wie OpenFileDialog, FontDialog, PrintDocument etc., die ebenfalls nicht direkt in ein Formular eingefügt werden können und somit im Formular unsichtbar bleiben. Im Gegensatz zu VB6 handelt es sich dabei aber um ganz gewöhnliche Klassen, die nur von der Entwicklungsumgebung so behandelt werden, als wären sie Steuerelemente. Das nimmt Ihnen die Arbeit ab, ein Objekt dieser Klasse selbst zu erzeugen (und vermindert die Umstiegsprobleme). Anders als in VB6 können Sie die VB.NET-Pseudosteuerelemente aber auch ohne den Umweg über ein Formular direkt per Code erzeugen und anwenden. Manche aus VB6 vertrauten unsichtbaren Steuerelemente stehen in VB.NET nicht mehr zur Verfügung. Beispielsweise gibt es kein direktes Gegenstück zum WinSock-Steuerelement. Vergleichbare Funktionen stehen unter .NET aber sehr wohl zur Verfügung (nämlich durch die Klassen System.Net.* und System.Net.Sockets.*).
Neue Steuerelemente Mit VB.NET sind manche Steuerelemente verschwunden, aber es gibt auch einige neue. Die folgende Liste zählt die wichtigsten davon auf:
96
•
3 Von VB6 zu VB.NET
CheckedListBox: Diese Variante zur ListBox stattet jedes Listenelement mit einem Kon-
trollkästchen aus. Das Steuerelement eignet sich damit hervorragend, wenn Optionen aus einer umfangreichen Liste ausgewählt werden sollen. •
DomainUpDown: Das Steuerelement ermöglicht die Auswahl einer Zeichenkette aus einer vorgegebenen Liste. Die Funktionsweise ist ähnlich wie bei einer ComboBox, allerdings gibt es zwei Pfeilbuttons zur Auswahl des vorigen oder nächsten Listeneintrags.
•
ErrorProvider: Das Steuerelement zeigt neben einem Steuerelement mit einem Eingabefehler ein kleines rotes Icon mit einem Ausrufezeichen an.
•
HelpProvider: Das Steuerelement ermöglicht das automatische Anzeigen eines Hilfetexts, wenn der Anwender F1 drückt.
•
LinkLabel: Das Steuerelement enthält einen Link, z.B. eine Web- oder E-Mail-Adresse oder einen Link auf eine lokale Datei. Beim Anklicken erscheint der Standard-Browser bzw. ein Mail- oder News-Programm.
•
NotifyIcon: Das Steuerelement ermöglicht die Anzeige eines Icons im Icon-Bereich der Taskbar. Das ist vor allem für Hintergrundprogramme praktisch, deren Benutzerober– fläche normalerweise unsichtbar ist.
•
Panel: Das Steuerelement dient als Container für andere Steuerelemente. Anders als GroupBox enthält es keine Beschriftung und ist durchsichtig. Dafür kann der Innenbe-
reich mit Scroll-Balken ausgestattet werden. •
Splitter: Das Steuerelement dient als Fensterteiler. Es wird als verschiebbare Linie (bzw. als schmaler Balken) zwischen zwei angedockten Steuerelementen angezeigt (wie beim Windows-Explorer zwischen dem Verzeichnisbaum und der Detailansicht der einzelnen Dateien).
•
ToolTip: Das Steuerelement verwaltet die ToolTip-Texte aller anderen Steuerelemente im
Formular. •
Steuerelemente zur Steuerung eines Ausdrucks: Die Klasse PrintDocument sowie die Steuerelemente PrintDialog, PageSetupDialog, PagePreviewDialog sowie PagePreviewControl helfen dabei, den Code zum Ausdruck von Dokumenten mit einer ansprechenden Benutzeroberfläche zu versehen.
•
Sonstige neue Komponenten: Dieser Abschnitt beschreibt nur Steuerelemente zur Windows-Programmierung. Darüber hinaus bietet .NET in der Toolbox aber zahlreiche weitere neue Komponenten, die z.B. zur Entwicklung von Server-, Datenbank- oder ASP.NET-Anwendungen geeignet sind. Bei vielen dieser Komponenten handelt es sich um gewöhnliche .NET-Klassen. Ihre Platzierung in der Toolbox erleichtert sowohl die Suche als auch die Anwendung ein klein wenig, weil die Initialisierung (Dim x As New klassenname) und Einstellung von Eigenschaften quasi automatisch – d.h. in einem von der Entwicklungsumgebung erzeugten Codeabschnitt – erfolgt.
3.2 Unterschiede zwischen VB6 und VB.NET
97
Geänderte Eigenschaften, Methoden, Ereignisse Der Platz in diesem Buch reicht nicht aus, um sämtliche geänderten Eigenschaften, Methoden und Ereignisse aller Steuerelemente aufzulisten. Die Grundformel lautet leider: Mindestens die Hälfte aller Namen von Schlüsselwörtern haben sich geändert; in vielen Fällen hat sich auch das Verhalten, das Datenformat etc. geändert! Dieser Abschnitt beschränkt sich daher auf einige fundamentale Änderungen bzw. Neuerungen, die für die meisten Steuerelemente relevant sind: •
Defaulteigenschaften: In VB6 konnte die wichtigste Eigenschaft eines Steuerelements ohne explizite Nennung verwendet werden. Text1="abc" veränderte etwa die TextEigenschaft einer Textbox. Die .NET-Steuerelemente kennen allerdings generell keine Defaulteigenschaften mehr. Statt Text1="abc" müssen Sie jetzt Text1.Text="abc" schreiben.
•
Positionierung von Steuerelementen: Um Steuerelemente bei einer Änderung der Fenstergröße neu zu positionieren, musste in VB6 die Form_Resize-Ereignisprozedur verwendet werden. In VB.NET ist das aufgrund der neuen Eigenschaften Anchor und Dock nur noch ganz selten erforderlich. Anchor ermöglicht es, den Abstand eines Steuerelements zu den Fensterrändern zu fixieren. Per Default ist dies nur für den linken und oberen Abstand der Fall, d.h., die Steuerelemente verhalten sich wie gewohnt. Indem auch die beiden anderen Ränder einbezogen werden, sind aber Steuerelemente möglich, die sich ohne Zusatzcode automatisch an die Fenstergröße anpassen bzw. die am rechten oder unteren Fensterrand scheinbar kleben bleiben.
Eine ähnliche Wirkung hat Dock: Damit kann jedes Steuerelement an einen der vier Ränder eines Fensters gebunden (angedockt) werden. Es nimmt dort automatisch die volle Fensterbreite bzw. -höhe an. •
HelpXxx-Eigenschaften: In VB6 war (fast) jedes Steuerelement mit einigen HelpXxxEigenschaften ausgestattet, die angaben, welches Hilfedokument durch F1 angezeigt
werden sollte. In VB.NET fehlen diese Eigenschaften, dafür gibt es ein neues HelpProvider-Steuerelement, das die Verbindung zwischen den Steuerelementen herstellt. Die Anwendung des HelpProviders ist nicht wesentlich komplizierter, allerdings ist es nicht mehr möglich, ein konkretes Hilfedokument durch dessen ID-Nummer zu identifizieren (d.h., es gibt kein Analogon zur VB6-Eigenschaft HelpContextID). Der Umstand, dass mit VB.NET eine im Vergleich zu VB6 beinahe unveränderte Version des HTML-Help-Workshops (Copyright 1998!) mitgeliefert wird, beweist, dass Microsoft das Thema eigene Hilfe auch in .NET stiefmütterlich behandelt. Die einzigen, die sich darüber freuen dürfen, sind die Anbieter so genannter Help-Authoring-Systeme ... •
Tag-Eigenschaft/Vererbung: In VB6 bot die Tag-Eigenschaft der meisten Steuerelemente die Möglichkeit, zusammen mit dem Steuerelement eine Zeichenkette zu speichern.
98
3 Von VB6 zu VB.NET
In VB.NET existiert diese Eigenschaft weiterhin. Sie kann nun Objekte beliebiger Klassen speichern. In vielen Fällen ist eine Verwendung von Tag aber gar nicht mehr notwendig. Viel eleganter ist es, durch Vererbung einfach ein neues Steuerelement zu deklarieren, das zusätzliche Eigenschaften und Methoden zur Verwaltung interner Daten hat. •
ToolTip-Eigenschaft: In VB6 hatten die meisten Steuerelemente eine ToolTip-Eigenschaft, mit der ein Infotext eingestellt werden konnte. Dieser Text erscheint automatisch in einer kleinen gelben Box, wenn die Maus eine Weile über dem Steuerelement verharrt.
In VB.NET fehlt die ToolTip-Eigenschaft bei den meisten Steuerelementen, dafür gibt es aber ein neues ToolTip-Steuerelement. Um ToolTips anzuzeigen, muss für jedes Steuerelement die Anweisung tooltipObject.SetToolTip(steuerelement, "mein Tooltip-Text") ausgeführt werden. Mit anderen Worten: ToolTips werden weiterhin unterstützt, die Initialisierung ist aber ungleich komplizierter als bisher und kann nur per Code erfolgen (nicht im Eigenschaftsfenster). Irgendein Vorteil dieser Änderung ist nicht zu erkennen. •
Steuerelementfelder: VB6 bot die Möglichkeit, mehreren Steuerelementen denselben Namen zu geben. Diese Steuerelemente konnten weiterhin über eine Indexnummer unterschieden werden. Der Vorteil dieser Vorgehensweise: Die Steuerelemente konnten mit gemeinsamen Ereignisprozeduren ausgestattet werden, in Schleifen gemeinsam manipuliert werden etc. In VB.NET sind Steuerelementfelder in dieser Form leider ersatzlos verschwunden. Zwar können Sie weiterhin über Controls auf alle Steuerelemente eines Formulars zugreifen und per Code mehrere Steuerelemente mit einer gemeinsamen Ereignisprozedur ausstatten – aber die Eleganz und Einfachheit von VB6 ersetzt das nicht. (Tipps, wie Sie mehrere gleichartige Steuerelemente effizient verwalten, finden Sie in Abschnitt 14.11.)
MDI-Anwendungen MDI-Anwendungen (zur Realisierung des multiple document interface) werden im Prinzip wie in VB6 unterstützt, auch wenn die Methoden und Eigenschaften zur Verwaltung der Fenster neue Namen bekommen haben. Insbesondere wird das Hauptfenster nun durch IsMdiContainer = True gekennzeichnet; bei den Subfenstern muss MdiParent auf das Hauptfenster verweisen. Vollkommen geändert hat sich allerdings die Menüverwaltung bei MDI-Anwendungen: Nach wie vor können sowohl das Haupt- als auch das Subfenster mit eigenen Menüs ausgestattet werden, und nach wie vor wird das Menü ausschließlich im Hauptfenster angezeigt. Damit enden die Gemeinsamheiten aber schon. Nun zu den Unterschieden: •
In VB6 war es so, dass das Hauptfenstermenü nur so lange angezeigt wurde, bis zumindest ein Subfenster mit einem eigenen Menü geöffnet wird. Ab diesem Zeitpunkt wurde im Hauptfenster nur noch das Menü des (gerade aktiven) Subfensters angezeigt.
3.2 Unterschiede zwischen VB6 und VB.NET
•
99
In .NET werden die Menüs von Haupt- und Subfenster dagegen kombiniert: Im Hauptfenster werden zuerst die Einträge des Hauptfensters, daran anschließend die Einträge des gerade aktiven Subfensters angezeigt.
Änderungen bei anderen Aspekten der Windows-Programmierung VB6
VB.NET
Tastatureingaben: In VB6 besteht die Möglichkeit, in der KeyPressProzedur den Code des eingegebenen Zeichens zu verändern (also beispielsweise ein x durch ein y zu ersetzen).
Diese Möglichkeit gibt es in .NET in dieser Form leider nicht mehr. Dasselbe Ziel kann über Umwege aber dennoch erreicht werden (z.B. durch die direkte Veränderung der TextEigenschaft des Steuerelements oder unter Zuhilfenahme von SendKeys.)
Tastatureingaben simulieren: Zur Simulation von Tastatureingaben steht die SendKeys-Methode zur Verfügung.
Statt SendKeys("xxx") müssen Sie nun SendKeys.Send("xxx") ausführen. Durch einen optionalen Parameter können Sie eine sofortige Verarbeitung der Eingabe erreichen.
Zwischenablage: VB6 stellt zum Zugriff auf die Zwischenablage das Clipboard-Objekt zur Verfügung.
Die Clipboard-Klasse von .NET ist zwar vollständig inkompatibel mit dem aus VB6 vertrauten Clipboard-Objekt, bietet aber im Wesentlichen dieselben Möglichkeiten.
Drag&Drop: In VB6 gibt es verschiedene Drag&Drop-Varianten. Drag&Drop zum Verschieben ganzer Steuerelemente innerhalb des Programms, automatisches OLEDrag&Drop zum Verschieben von Daten aus Steuerelementen sowie manuelles OLE-Drag&Drop, mit dem der gesamte Prozess des Datenaustauschs kontrolliert werden konnte.
Mit .NET ist auch hier alles anders geworden. Die drei links beschriebenen Varianten sind zu einem Drag&Drop-Verfahren vereinheitlicht worden, das am ehesten dem manuellen OLE-Drag&Drop entspricht. Diese Vereinheitlichung ist an sich erfreulich. Positiv ist auch der Umstand, dass der Datenaustausch nun dieselben Mechanismen wie beim Umgang mit der Zwischenablage verwendet. Mit dem neuen Drag&Drop lassen sich allerdings nicht alle Effekte der drei ehemaligen Drag&Drop-Varianten erzielen. Beispielsweise funktioniert Drag&Drop innerhalb eines Textfelds nicht mehr. Auch der Programmieraufwand ist zum Teil höher als bisher.
100
3.2.5
3 Von VB6 zu VB.NET
Grafikprogrammierung und Drucken
VB6
VB.NET
VB6 bietet zur Grafikprogrammierung einige einfache Steuerelemente (Shapes) sowie einige rudimentäre Methoden (Cls, Line, Circle etc.). Soweit Sie mit diesen Elementen das Auslangen finden, ist die Grafikprogrammierung relativ einfach.
Die Grafikprogrammierung in VB.NET ist zu 100 Prozent inkomaptibel zu VB6. Die vertrauten Steuerelemente funktionieren anders bzw. es gibt sie nicht mehr.
Sehr oft ist es aber erforderlich, auf GDI-Funktionen des Betriebssystems auszuweichen – und dann wird die Grafikprogrammierung mühsam. (GDI steht für Graphics Device Interface.)
Dafür stehen Ihnen in VB.NET zahllose System.Drawing-Klassen zur Verfügung, die das Fundament für das neue Grafiksystem GDI+ bilden (das zum alten GDI natürlich ebenfalls vollständig inkompatibel ist). VB.NET bietet damit neue Möglichkeiten zur Grafikprogrammierung, die in praktisch jeder Hinsicht besser sind als die von VB6 – mehr dazu in Kapitel 16. Allerdings vergessen Sie am besten vorher alles, was Sie bisher zum Thema Grafikprogrammierung wussten!
Drucken VB6
VB.NET
Die Druckerunterstützung in VB6 ist miserabel. Die Print-Klasse ist für professionelle Anwendungen zu wenig leistungsstark; die für Datenbankanwendungen mitgelieferte Version von Crystal Reports vollkommen veraltet und ADO-inkompatibel.
VB.NET stellt in dieser Hinsicht eine große Verbesserung dar. Die unselige Print-Klasse gibt es nicht mehr (und keiner wird ihr eine Träne nachweinen). Stattdessen gibt es nun die PrintDocument-Klasse, die den Druckvorgang über vier Ereignisse steuert, zahlreiche weitere Klassen zur Verwaltung von Druckern und ihren Eigenschaften sowie einige neue Steuerelemente zur Auswahl des Druckers, zur Einstellung des Seitenlayouts und zur Durchführung einer Seitenvorschau. Die mit VS.NET Professional und Enterprise mitgelieferte Version von Crystal Reports ist aktuell und erstmals ordentlich in die Entwicklungsumgebung integriert.
PrintForm: Mit dieser Methode
PrintForm steht leider nicht mehr zur Verfügung.
können Sie in VB6 das Formular als Bitmap ausdrucken.
Wie Sie das Formular dennoch ausdrucken können, zeigt ein Beispielprogramm in Abschnitt 16.5.10.
3.2 Unterschiede zwischen VB6 und VB.NET
3.2.6
101
Umgang mit Verzeichnissen und Dateien
VB6
VB.NET
Zum Zugriff auf Verzeichnisse und Dateien können wahlweise die aus früheren VB-Versionen vertrauten Kommandos (FileCopy, Open etc.) oder die in VB6 eingeführte FSOBibliothek verwendet werden.
Zum Umgang mit Verzeichnissen und Dateien sind die System.IO-Klassen vorgesehen, die in Kapitel 10 ausführlich beschrieben werden. Daneben können aber sowohl die alten Dateikommandos als auch die Objekte der FSOBibliothek (COM) weiterhin benutzt werden. Die VB6-Kommandos Get und Put haben neue Namen bekommen (FileGet und FilePut), funktionieren aber sonst wie gewohnt. App.Path zur Ermittlung des aktuellen
Verzeichnisses steht nicht mehr zur Verfügung. Dafür gibt es aber diverse neue Wege, um das aktuelle Verzeichnis und andere spezielle Verzeichnisse (das Windows-Verzeichnis etc.) zu ermitteln – siehe Abschnitt 10.3.4. ChDir ändert nur das aktuelle
ChDir ändert nun auch das Laufwerk (wenn dieses
Verzeichnis, nicht aber das Laufwerk.
angegeben wird). Im Gegensatz zu früher ist es nicht mehr erforderlich, dazu auch ChDrive auszuführen.
3.2.7
Fehlerabsicherung und Debugging
On Error: Die aus VB6 vertrauten On-Error-Anweisungen stehen in VB.NET weiterhin zur Verfügung. Besser lesbaren Code erhalten Sie aber, wenn Sie die neue Try-Catch-Konstruktion zur Fehlerabsicherung verwenden. (Es ist nicht möglich, On Error und Try Catch innerhalb einer Prozedur zu kombinieren.) Ausnahmen (exceptions): Auch wenn es On Error noch immer gibt – hinter den Kulissen hat sich viel geändert. Insbesondere werden Fehler nun durch so genannten Exceptions zwischen verschiedenen Programmteilen weitergereicht. Exceptions basieren auf einer neuen .NET-Klasse; die davon abgeleiteten Objekte helfen, die Natur des Fehlers genau zu beschreiben. Debugger: Im Zuge der Erneuerung der Entwicklungsumgebung wurden natürlich auch alle Debugging-Elemente erneuert. Die meisten Bedienungselemente – etwa zur Verwaltung von Haltepunkten, zum Analysieren von Variablen etc. – sind aber ganz ähnlich wie bisher zu bedienen und wurden in vielen Details verbessert. Eine der wichtigsten Neuerungen besteht im Thread-Fenster, das auch in Multithreading-Programmen die Fehlersuche möglich macht.
102
3 Von VB6 zu VB.NET
Direktfenster: Einer der wenigen Punkte, wo Sie bei der neuen Debugging-Umgebung Einbußen im Vergleich zu VB6 hinnehmen müssen, ist das Direktfenster. Dieses Fenster heißt jetzt Befehlsfenster (ANSICHT|ANDERE FENSTER) und steht leider nur noch in einer sehr eingeschränkten Form zur Verfügung. Zwar können Sie noch immer Variablen anzeigen und verändern, allerdings lässt der PrintOperator ? nur noch die Angabe eines einzigen Ausdrucks zu. Komplexe Operationen (etwa das Ausführen einer Schleife, um alle Elemente eines Felds anzuzeigen) sind nicht mehr möglich. Die automatische Vervollständigung von Eigenschaften und Methoden (Intellisense) funktioniert ebenfalls nicht mehr. Generell stehen viele Funktionen erst dann zur Verfügung, wenn gerade ein Programm ausgeführt wird (das momentan unterbrochen ist). Dafür kennt das Befehlsfenster jetzt einen neuen Befehlsmodus. In diesem Modus können Sie Befehle zur Steuerung der Entwicklungsumgebung ausführen. Beim Schreiben dieses Buchs habe ich diese neue Funktion allerdings kein einziges Mal benötigt. Codeänderungen während des Debuggings: Dramatische Veränderungen gibt es leider, was die Veränderung des Codes während der Programmausführung betrifft. Anders als in VB6 ist es nicht mehr möglich, beim Auftreten eines Fehlers den Code zu ändern und das Programm dann fortzusetzen. Stattdessen muss das Programm beendet und anschließend neu gestartet werden. Das Fehlen dieses VB6-Features zählt zu den größten Quellen von Ärger und Wut unter eingefleischten VB6-Fans. Entsprechende Diskussionen in den vb.net-Diskussionsforen haben epische Längen erreicht. Selbst Microsoft hat zugegeben, dass dies ein Rückschritt sei und dass es Überlegungen gäbe, dieses Feature in kommenden Versionen wieder einzuführen. Ob Microsoft diesen edlen Vorsatz verwirklichen kann, ist allerdings nicht so sicher: VB.NET ist zu einer echte Compiler-Sprache geworden, was viele Vorteile, aber auch manche Nachteile mit sich bringt. Und ein Nachteil besteht eben darin, dass Änderungen in einem bereits compilierten Programm an sich unmöglich sind, ohne das Programm zu verändern. Wenn Microsoft das Unmögliche doch zustande bringt, dann nur durch massive Veränderungen innerhalb der .NET-Infrastruktur und insbesondere im MSIL-Compiler; derartige Änderungen wären vermutlich mit neuen Inkompatibilitäten verbunden, und die kann sich Microsoft nach der mühsamen Migration von VB6 zu VB.NET in naher Zukunft eigentlich nicht leisten. Meine pragmatische Empfehlung lautet daher: Gewöhnen Sie sich ein exakteres Programmieren an! VB6 hatte bei vielen VB-Programmierern den Effekt, dass Code recht flott geschrieben und getestet wurde; wenn er dann – beinahe erwartungsgemäß – nicht funktionierte, wurde er im laufenden Programm optimiert. (Man kann diese Art der Programmentwicklung auch mit rapid prototyping umschreiben – dann klingt es weniger negativ.)
3.2 Unterschiede zwischen VB6 und VB.NET
VERWEIS
3.2.8
103
Datenbank- und Internet-Programmierung
Datenbank- und Internet-Anwendungen sind aus Platzgründen nicht Thema dieses Buchs, sondern werden in einem eigenen Buch beschrieben. Aus diesem Grund sind die in diesem Abschnitt zusammengestellten Informationen eher knapp und grundsätzlich gehalten. Informationen über den Inhalt und den Erscheinungstermin des neuen Buchs finden Sie auf meiner Website unter http://www.kofler.cc.
Von ADO zu ADO.NET Die große Neuerung von VB6 im Datenbankbereich war die Datenbankbibliothek ADO (ActiveX Data Objects). Während die mit VB6 ursprünglich mitgelieferte Version noch unausgegoren war, hat sich das im Laufe mehrere Service Packs und neuer ADO-Versionen allmählich zum Besseren gewandt und ADO ist gewissermaßen erwachsen geworden. Mit VB.NET gehört ADO allerdings schon wieder zum alten Eisen. Die neue Zauberformel lautet nun ADO.NET. Obwohl das Kürzel ADO noch immer ActiveX-Technologie vermuten lässt, ist ADO.NET eine komplette, .NET-konforme Neuentwicklung. ADO.NET ist Teil des .NET-Frameworks und wird über zahlreiche Klassen der Bibliothek System.Data verwendet. Einmal abgesehen davon, dass sich fast alle Namen von Schlüsselwörtern geändert haben und zahllose neue Funktionen dazugekommen sind, hat auch ein fundamentaler Paradigmenwechsel stattgefunden: •
Bei ADO war es so, dass zuerst eine Datenbankverbindung hergestellt wurde und diese dann für längere Zeit geöffnet blieb, um Datenbankabfragen auszuführen, neue Daten in der Datenbank zu speichern etc.
•
ADO.NET ist dagegen verbindungslos. Wenn Daten bearbeitet werden sollen, wird kurzzeitig eine Verbindung zur Datenbank hergestellt und die Daten werden an den Client übertragen. Anschließend wird die Verbindung sofort wieder beendet. Nun können die Daten am Client bearbeitet werden. Wenn irgendwelche Änderungen gespeichert werden sollen, dann muss neuerlich eine Verbindung hergestellt werden.
Beide Vorgehensweisen haben Vor- und Nachteile. Der Vorteil von ADO besteht darin, dass Änderungen in der Datenbank beinahe sofort auch im Client-Programm bemerkbar werden (das Recordset-Objekt kann also aktualisiert werden). Wenn mehrere Anwender gleichzeitig gemeinsame Daten bearbeiten, kann das eine wesentliche Hilfe sein. (Wenn Anwender A den letzten freien Platz für einen Flug reserviert hat, bemerkt Anwender B dies sofort – vorausgesetzt die Anwendung wurde entsprechend programmiert.)
104
3 Von VB6 zu VB.NET
Der Vorteil von ADO.NET besteht darin, dass mit Datenbankverbindungen viel sparsamer umgegangen wird. Derartige Verbindungen sind ein kostbares Gut; sie beanspruchen am Datenbank-Server Speicher und Rechenzeit. Je kürzer Datenbankverbindungen aufrechterhalten werden, desto mehr Anwender können gleichzeitig auf die Datenbank zugreifen und desto effizienter können diese Zugriffe verarbeitet werden. (Für das obige Beispiel einer Flugreservierung bedeutet das, dass Anwender B beim Versuch, den letzten Flug ebenfalls zu reservieren, eine Fehlermeldung erhält – also zu einem viel späteren Zeitpunkt als bei einem entsprechenden ADO-Programm.) Vereinfacht könnte man sagen, dass das Programmiermodell von ADO.NET dem Gedanken des Internets näher kommt und sich daher besonders gut zur Programmierung von Internet-Anwendungen (ASP.NET, Webservices) eignet. ADO spielt seine Vorteile dagegen eher dann aus, wenn herkömmliche, interaktive Windows-Anwendungen entwickelt werden sollen. Microsoft sieht ADO.NET nicht als Ersatz für ADO, sondern als Ergänzung. Und tatsächlich kann die ADO-Bibliothek wie jede ActiveX-Bibliothek unter VB.NET weiter verwendet werden. Wenn Sie nun aber versuchen, ADO in Ihren VB.NET-Programmen zu nutzen, werden Sie rasch bemerken, dass die grundsätzliche Kompatibilität Ihnen kaum weiterhilft: So sind die neuen Windows.Forms-Steuerelemente grundsätzlich nicht ADO-kompatibel. (Und wie bereits erwähnt, ist ADO speziell für gewöhnliche Windows-Anwendungen geeignet.) Die unter VB6 übliche Schreibweise recordsetobject!spaltenname ist nicht mehr zulässig; stattdessen müssen Sie jetzt recordsetobject.Fields("spaltenname") schreiben. Die Liste der Probleme ließe sich fortsetzen, das Fazit lautet aber kurz und bündig: Wenn Sie bei ADO bleiben möchten (dafür gibt es bei bestimmten Anwendungsszenarien gute Gründe), sollten Sie in den meisten Fällen auch gleich bei VB6 bleiben. VB.NET bietet für Datenbankentwickler viele neue und tolle Funktionen – aber diese Funktionen stehen nur dann zur Verfügung, wenn ADO.NET eingesetzt wird. ADO-Anwender gewinnen durch den Umstieg auf VB.NET dagegen so gut wie nichts, müssen aber eine Menge Kompatibilitätsprobleme in Kauf nehmen.
Entwicklung von dynamischen Webseiten (ASP.NET) Beginnend mit VB4 hat Microsoft mit jeder neuen Version eine neue Technologie eingeführt, so dass unter VB6 schließlich die folgenden Technologien unterstützt wurden. •
ActiveX-Dokumente
•
ActiveX-Steuerelemente
•
DHTML-Anwendungen
•
WebClasses (IIS-Anwendungen)
•
ASP-Seiten
Wirklich durchgesetzt haben sich davon einzig ASP-Seiten – und ausgerechnet die hatten eigentlich wenig mit dem klassischen VB zu tun. ASP-Seiten enthalten nämlich nur VBScript-Code, der in der VB6-Entwicklungsumgebung weder entwickelt noch ausgeführt
3.2 Unterschiede zwischen VB6 und VB.NET
105
werden kann. (Allerdings können in ASP-Seiten ActiveX-Komponenten genutzt werden, die wiederum mit VB6 entwickelt werden können.) In VB.NET gibt es keine einzige der fünf genannten Technologien mehr! Stattdessen wurde mit ASP.NET eine weitere Technologie eingeführt, die die Vorteile von herkömmlichen ASP-Seiten mit denen von WebClasses verbindet (siehe auch Abschnitt 2.5). Mit ASP.NET ist Microsoft ohne Zweifel ein großer Wurf gelungen, nur dürfen Sie eben nicht erwarten, dass Sie auch nur eine einzige Zeile Code aus einem mit VB6 entwickelten Webprojekt übernehmen können – ganz egal, auf welche der vielen Technologien Sie gesetzt haben.
Web-Services Web-Services sind eine weitere Neuerung in .NET. Sie ermöglichen eine standardisierte Kommunikation zwischen Internet-Servern über das HTTP-Protokoll, aber ohne die Verwendung von HTML-Dokumenten. (Die Daten werden vielmehr im XML-Format ausgetauscht.)
XML-Funktionen XML ist ein strukturiertes Textformat, das vor allem zum Austausch von Daten im Internet eingesetzt wird. Die .NET-Bibliothek System.XML bietet zahllose Klassen zum Erzeugen und Verarbeiten von XML-Daten. Diese Klassen können Sie natürlich auch für herkömmliche Anwendungen oder für Datenbankprogramme einsetzen.
HTML-Dokumente anzeigen, E-Mails verwalten In VB6 konnten Sie mit dem vom Internet Explorer stammenden WebBrowser-Steuerelement HTML-Dokumente anzeigen und mit den MAPISession- bzw. MAPIMessage-Steuerelementen E-Mails versenden. Unter VB.NET können Sie das WebBrowser-Steuerelement als ActiveX-Steuerelement weiterverwenden, es gibt aber keine eigene .NET-Variante davon. Die MAPI-Steuerelemente wurden ersatzlos gestrichen. Methoden zum Versenden von E-Mails finden Sie in den Klassen System.Web.Mail.* (Bibliothek System.Web).
Low-Level-Programmierung Wenn Sie Daten direkt auf Basis der Protokolle HTTP, FTP, TCP, UDP etc. austauschen möchten, standen Ihnen dazu in VB6 die Steuerelemente WinSock und Inet zur Verfügung. Diese Steuerelemente gibt es nicht mehr, dafür enthält die .NET-Bibliothek aber zahllose Klassen, die zum selben Zweck viel mehr und vor allem viel besser durchdachte Funktionen zur Verfügung stellen. Alten Code können Sie freilich auch hier nicht einmal ansatzweise übernehmen.
106
3.2.9
3 Von VB6 zu VB.NET
Sonstiges
VB6
VB.NET
App-Objekt: Die verschiedenen Eigenschaften dieses globalen Objekts geben Auskunft über das laufende Programm (Name, Verzeichnis etc.)
Das Objekt gibt es nicht mehr. Ein Teil der Informationen kann aber mit den Eigenschaften und Methoden verschiedener .NET-Klassen ermittelt werden, z.B. System.Reflection, System.Environment und System.Windows.Forms.Application.
API-Funktionen: Fast alle Betriebssystemfunktionen können in VB-Programmen benutzt werden. Allerdings müssen sie vorher mit Declare deklariert werden.
Declare steht weiterhin zur Verfügung, wird aber
An API-Funktionen werden grundsätzlich ANSI-Zeichenketten übergeben, selbst dann, wenn die API-Funktionen in der Lage wären, Unicode-Zeichenketten zu verarbeiten.
viel seltener gebraucht, weil fast alle Betriebssystemfunktionen nun über die .NETKlassenbibliothek zur Verfügung stehen. Wenn der Aufruf von API-Funktionen aber unvermeidlich ist, sind große Änderungen im Vergleich zu VB6-Code erforderlich: zum einen, weil sich alle elementaren Datentypen geändert haben (Long, Integer), zum anderen, weil Types durch Structures mit einer anderen Syntax ersetzt wurden. Wesentlich verbessert hat sich der Umgang mit Zeichenketten. VB.NET ist nun in der Lage, Zeichenketten im Unicode-Zeichensatz an die API-Funktion zu übergeben. ANSI-Zeichenketten werden weiterhin unterstützt (je nachdem, wie die API-Funktion mit Declare deklariert wird).
DDE: DDE steht für Dynamic Data Exchange und ermöglichte die Steuerung externer Programme. DDE wird allerdings nur noch von wenigen, ziemlich alten WindowsProgrammen unterstützt und hat daher eine geringe Bedeutung.
DDE wird von VB.NET nicht mehr unterstützt.
DoEvents: Dieses Schlüsselwort
DoEvents steht in Windows-Anwendungen
bewirkt in VB6, dass die Ausführung der aktuellen Ereignisprozedur vorübergehend unterbrochen wird, um andere Ereignisse (z.B. Mausklicks) festzustellen und zu verarbeiten.
weiterhin zur Verfügung, muss nun aber mit Application.DoEvents aufgerufen werden. In vielen Fällen bieten die neuen Möglichkeiten zur Multithreading-Programmierung bessere (effizientere) Wege, um im Hintergrund länger andauernde Operationen durchzuführen, ohne die Bedienbarkeit von Programmen einzuschränken.
3.2 Unterschiede zwischen VB6 und VB.NET
107
VB6
VB.NET
Interne Projektverwaltung: Ein VBProjekt besteht aus relativ wenigen Dateien, im einfachsten Fall aus einer Projektdatei (*.vbp) sowie einigen Formular- und Codedateien (*.frm und *.bas).
Bei jedem noch so kleinen VB.NET-Projekt werden unzählige Dateien und mehrere Verzeichnisse erzeugt. Aus diesem Grund besteht die Entwicklungsumgebung darauf, jedes Projekt in einem neuen Verzeichnis zu speichern (was natürlich auch schon unter VB6 empfehlenswert war). Dieses Verzeichnis muss beim Erstellen eines Projekts angegeben werden. Wenn Sie diesen Teil des Dialogs nicht beachten, erzeugt die Entwicklungsumgebung selbst ein neues Verzeichnis (in Eigene Dateien\Visual StudioProjekte).
Setup-Programm: In VB6 gibt es einen Installationsassistenten (den package and deployment wizard, kurz PDW), um Setup-Programme für VB6-Programme zu entwickeln.
Auch VB.NET bietet vergleichbare Funktionen, die aber in jeder Hinsicht vollkommen anders organisiert sind. Das gilt sowohl für die Konfiguration (für die es eigene Projekttypen gibt) als auch für die interne Logik der Weitergabe (die nun auf dem Windows Installer basiert).
COM- und ActiveX-Kompatiblität Fast ein Jahrzehnt war OLE alias COM alias ActiveX das vorherrschende Objektmodell im Microsoft-Weltbild. Mit .NET hat sich das geändert. Jetzt heißt die neue Devise: Mit .NET wird alles besser, ActiveX ist alt, überholt und ungeeignet. Dieselben Argumente, mit denen vor ein paar Jahren ActiveX beworben wurden, sind jetzt als Werbeargumente für .NET zu hören. Dabei bestreite ich keineswegs, dass .NET jede Menge Vorteile hat. Tatsache ist aber auch, dass es beinahe unendlich viel Code gibt, der auf ActiveX basiert. Wieweit ist VB.NET also kompatibel zu ActiveX? ActiveX-Steuerelemente und -Bibliotheken: Diese Komponenten können unter VB.NET weiterhin verwendet werden. Sobald Sie einen Verweis auf eine derartige Komponente einrichten, erzeugt die Entwicklungsumgebung automatisch eine so genannte wrapperBibliothek, die die Schnittstelle zwischen .NET und COM herstellt. In der Praxis hat es bei den meisten Experimenten mit ActiveX-Komponenten kleinere Probleme gegeben, die aber meist mit etwas Experimentieren zu lösen waren. ActiveX-Automation: Manche ActiveX-/COM-/OLE-Programme können durch Automation von außen gesteuert werden (z.B. alle Programme aus dem Microsoft-Office-Paket). In VB.NET können Sie diesen Steuerungsmechanismus wegen der COM-Kompitibilität weiterhin nutzen. Dabei gibt es allerdings einige Einschränkungen: •
Bevor die Klassenbibliothek eines externen Programms verwendet werden kann, muss eine so genannte Wrapper-Bibliothek erstellt werden, die sich um den Übergang zwischen .NET und COM kümmert. Die .NET-Entwicklungsumgebung kümmert sich automatisch um die Erzeugung dieser Wrapper-Bibliothek. Leider gibt es dabei bei
108
3 Von VB6 zu VB.NET
manchen Bibliotheken arge Probleme, d.h., Klassen fehlen, Typen werden nicht richtig zugeordnet etc. In solchen Fällen ist es das Beste, late binding einzusetzen. (Dazu muss Option Strict Off verwendet werden!) •
Da das OLE-Steuerelement unter VB.NET nicht mehr zur Verfügung steht, kann das zu steuernde Programm nicht mehr innerhalb eines Fensters des VB.NET-Programms angezeigt werden. (Da diese Funktion in VB6 meistens auch nicht richtig funktioniert hat, ist der Verlust des OLE-Steuerelements leicht zu verschmerzen.)
VERWEIS
Runtime.Interop: Wenn es Kompatibilitätsprobleme zwischen COM und .NET gibt, dann helfen eventuell die Klassen System.Runtime.Interop.* weiter.
Zum Thema COM- und .NET-Kompatibilität sind (bis jetzt) zwei englische Bücher erschienen: .NET and COM: The Complete Interoperability Guide von Adam Nathan und COM and .NET Interoperability von Andrew Troelsen. Der Umfang der beiden Bücher (zusammen 2500 Seiten) sollte klar machen, dass es zu diesem Thema mehr zu schreiben gibt als diesen kurzen Abschnitt. Wenn Sie also COM-Probleme mit .NET haben, sollten diese Bücher Ihre erste Anlaufstelle sein.
VB6-Kompatibilität Es sollte mittlerweile klar geworden sein, dass VB.NET in sehr vielen Punkten inkompatibel zu VB6 ist. Das bedeutet aber nicht, dass sich Microsoft nicht bemüht hätte, zumindest ein gewisses Maß an Kompatibilität zu erreichen. Dafür sind vor allem zwei Bibliotheken verantwortlich, von denen die erste offiziell unterstützt wird, die zweite dagegen nur für besondere Notfälle gedacht ist. •
Die Bibliothek microsoft.visualbasic.dll ist integraler Bestandteil aller VB.NET-Projekte (und kann theoretisch auch von anderen Programmiersprachen verwendet werden). Sie stellt im Namensraum Microsoft.VisualBasic eine Menge aus VB6 vertraute Methoden und Klassen zur Verfügung. Dazu zählen beispielsweise Zeichenkettenfunktionen (Left, Right etc.), herkömmliche Funktionen zum Dateizugriff, Funktionen zur Bearbeitung von Zahlen, Daten und Zeiten, vordefinierte Konstanten (vbCrLf, vbTab etc.), Konvertierungsfunktionen etc.
•
Die Bibliothek microsoft.visualbasic.compatibility.dll stellt im Namensraum Microsoft.VisualBasic.Compatibility.VB6 diverse Funktionen und Steuerelemente zur Verfügung, die in dieser Form in VB.NET nicht mehr existieren. Die Bibliothek ist nur als Hilfsmittel für den Migrationsassistenten gedacht. Die Dokumentation rät ausdrücklich davon ab, diese Bibliothek auch in neuen VB.NET-Projekten einzusetzen, weil die Bibliothek von künftigen VB.NET-Versionen unter Umständen nicht mehr unterstützt wird. Aus diesem Grund wird in diesem Buch auf eine Beschreibung der in dieser Bibliothek enthaltenen Schlüsselwörter verzichtet.
3.3 Der Migrationsassistent
3.3
109
Der Migrationsassistent
VB.NET ist eine neue Programmiersprache, deren einzige Gemeinsamkeit mit VB6 die Basissyntax ist. Auf beinahe 30 Seiten habe ich nun die wichtigsten Unterschiede zwischen VB6 und VB.NET aufgezählt, und ich betone nochmals, dass diese Aufzählung alles andere als komplett ist. Es sind wohl Marketing-Gründe, weswegen Microsoft dennoch das Unmögliche versucht und mit VS.NET Professional und Enterprise einen Migrationsassistenten mitliefert. Jedem Programmierer, der VB6 und VB.NET auch nur ein bisschen kennt, muss klar sein, dass eine automatische Codemigration nur in Ausnahemfällen gelingen kann. (Derartige Ausnahmefälle sind Algorithmen und Klassenbibliotheken, die weder eine Benutzeroberfläche haben noch irgendwelche ActiveX-Bibliotheken nutzen. Ich habe in den vielen Jahren, die ich mit VB schon programmiere, kein einziges derartiges Projekt erstellt.) Daher sollten Sie keine falschen Erwartungen an den Assistenten haben: Im Regelfall wird dieser Assistent kein lauffähiges VB.NET-Projekt liefern, sondern Code, der noch voller Fehler steckt, die Sie selbst beheben müssen. Das setzt ein gutes Wissen sowohl über VB6 als auch über VB.NET voraus. (Die trivialen Dinge – etwa die Konvertierung der Integerdatentypen – gelingen dem Assistenten erwartungsgemäß gut. Aber bei vielen Funktionen, bei denen eine Portierung aufwendig ist, macht der Assistent nicht einmal den Versuch einer Konvertierung. Dazu zählen z.B. alle Grafikausgaben.) Die Frage ist auch, welches Ziel die Konvertierung haben soll: Wenn es darum geht, vorhandenen VB6-Code weiter zu warten, ist eine Portierung auf VB.NET zwecklos. Es wird Tage, wenn nicht Wochen dauern, bis Sie ein größeres Projekt zuerst zum Laufen gebracht haben und es dann so lange ausführlich getestet haben, bis Sie sicher sind, dass es zumindest so fehlerfrei wie vorher läuft. Da ist es sinnvoller, zur Wartung und auch zur Realisierung einzelner neuer Funktionen weiterhin VB6 zu verwenden. Wenn Sie dagegen an einem neuen Projekt arbeiten und die eigentlichen Vorteile von .NET nutzen möchten, ist es vernünftiger, den Code von Grund auf neu zu entwickeln (auch wenn Sie schon einmal ein vergleichbares VB6-Projekt realisiert haben). Der Migrationsassistent ist naturgemäß nicht in der Lage, Code so umzuprogrammieren, dass statt der zahlreicher ActiveX-Komponenten gleichwertige bzw. bessere .NET-Bibliotheken eingesetzt werden. Über so viel Intelligenz verfügen nur Sie als Programmierer! Kurz und gut: So sehr ich die Leistung der Programmierer würdige, die den Migrationsassistenten entwickelt haben, so sehr bezweifle ich, dass es irgendein reales Anwendungsszenario für das Programm gibt. Das Programm versagt bereits bei fast allen meiner ziemlich trivialen Beispielprogramme aus dem VB6-Buch – wie soll das Programm dann mit realen (und sehr viel kompexeren) Anwendungen zurechtkommen?
110
3 Von VB6 zu VB.NET
VERWEIS
Weitere Informationen zum Migrationsassistenten finden Sie in der Online-Hilfe (suchen Sie nach Aktualisieren von Anwendungen) und im Internet: ms-help://MS.VSCC/MS.MSDNVS.1031/vbcon/html/vboriUpgradingApplications.htm http://www.devx.com/free/hotlinks/2002/ednote022002/ednote022002.asp
Auf den folgenden Seiten finden Sie grundsätzliche Informationen zur Migration von VB6 nach VB.NET: http://msdn.microsoft.com/vbasic/techinfo/articles/upgrade/guide.asp http://www.msdn.microsoft.com/library/en-us/dnvb600/html/vb6tovbdotnet.asp
Voraussetzungen VB6 muss am selben Rechner installiert sein. Der Grund hierfür besteht darin, dass der Migrationsassistent auf alle Steuerelemente und Bibliotheken zugreifen muss, die im jeweiligen VB6-Programm eingesetzt werden. Diese Voraussetzung wird am einfachsten durch eine VB6-Installation erfüllt. Ich habe auf meinem Rechner VB6 nach VB.NET installiert, ohne dass es zu Problemen gekommen ist. Nach Möglichkeit sollte aber dennoch die umgekehrte Reihenfolge vorgezogen werden.
•
Der Migrationsassistent wird nur mit den Professional- und Enterprise-Versionen mitgeliefert. Das Argument Microsofts lautet hierfür, dass sich VB.NET Standard an Einsteiger richtet, die mit dem Migrationsassistenten sicherlich überfordert sind und die im Übrigen auch keinen alten VB6-Code besitzen.
HINWEIS
•
In den VB.NET-Newsgruppen war mehrfach zu lesen, dass der Migrationsassistent anfänglich funktionierte, sich dann aber nicht mehr starten ließ. Abhilfe brachte erst ein neuerliches Ausführen des VS.NET-Installationsprogramms, um den Assistenten neu zu installieren. Ich kann aus eigener Erfahrung nichts dazu sagen – bei mir ließ sich der Assistent stets problemlos starten.
Anwendung Die Anwendung des Assistenten ist denkbar einfach: Sie öffnen in der Entwicklungsumgebung ein altes VB6-Projekt (also die *.vbp-Projektdatei). Damit wird der Assistent automatisch gestartet. Der Assistent fragt, in welches Verzeichnis er das neue Projekt speichern soll (siehe Abbildung 3.1) und beginnt dann mit der Konvertierung des Codes (die bei umfangreichen Projekten und langsamen Rechnern sehr lange dauern kann).
3.3 Der Migrationsassistent
111
Abbildung 3.1: Der Migrationsassistent
Das resultierende VB.NET-Projekt wird anschließend geöffnet. Sie können sich nun im Fenster AUFGABENLISTE sowie im Migrationsreport (der in HTML-Form Teil des Projekts ist, siehe Abbildung 3.2) über die Punkte informieren, die bei der Konvertierung Probleme bereitet bzw. Fehler verursacht haben.
Abbildung 3.2: Der Migrationsreport informiert über Fehler, die bei der Konvertierung des Projekts aufgetreten sind
Teil II
Grundlagen
4
Variablen- und Objektverwaltung
Nach einer Einführung in den Umgang mit Variablen (Deklaration, Zuweisung, Verwendung von Objektvariablen) beschreibt das Kapitel ausführlich die grundlegenden .NET-Datentypen, also beispielsweise Integer, Double, Date und String. Anschließend wird der Umgang mit Konstanten und Feldern beschrieben. Das Kapitel endet mit einem Ausflug in die Interna der Variablen-, Objekt- und Speicherverwaltung, wobei sich dieser Abschnitt eher an fortgeschrittene VB.NET-Programmierer richtet. 4.1 4.2 4.3 4.4 4.5 4.6
Umgang mit Variablen Variablentypen Konstanten Enum-Aufzählungen Felder Interna der Variablenverwaltung
116 127 132 134 139 146
VERWEIS
Dieses Kapitel stellt wirklich nur eine Einführung dar! Die folgende Liste gibt an, wo Sie weitere Detailinformationen finden: Lokale Variablen und Prozedurparameter: Abschnitt 5.3 Objekte nutzen: Kapitel 6 Eigene Klassen definieren: Kapitel 7 Eigene Datenstrukturen definieren (Structure): Abschnitt 7.3 Gültigkeitsbereiche von Variablen (scope): 7.9 Umgang mit Zahlen, Daten und Zeichenketten: Kapitel 8 Aufzählungen (Collections): Kapitel 9
116
4 Variablen- und Objektverwaltung
4.1
Umgang mit Variablen
4.1.1
Deklaration von Variablen
Variablen müssen vor ihrer ersten Verwendung mit Dim deklariert werden. Dieses Kommando kennt unglaublich viele Syntaxvarianten, die aber erst im weiteren Verlauf dieses Buchs beschrieben werden. Im einfachsten Fall deklariert Dim x, y die Variablen x und y. VB.NET verwendet dabei Object als Defaultdatentyp. In derartigen Variablen können Sie alle erdenklichen Daten (Zahlen, Zeichenketten etc.) speichern, ohne dass Sie sich über den Datentyp Sorgen machen müssen. Dim x, y ist unzulässig, wenn Sie Option Strict On verwenden (siehe Abschnitt 4.1.4). Per Default ist diese Option in VB.NET-Programmen aber nicht aktiv. Dim x, y As Integer deklariert die beiden Variablen explizit als Integer-Variablen (32 Bit mit
Vorzeichen). Der Vorteil gegenüber der Deklaration ohne die Angabe eines expliziten Datentyps besteht darin, dass Sie nun in x und y nicht versehentlich andere Daten speichern können. Außerdem bestehen aufgrund der expliziten Typangabe bessere Möglichkeiten zur Syntaxkontrolle, der resultierende Code wird effizienter, und der Platzbedarf im Speicher ist ein wenig geringer. Das folgende Miniprogramm deklariert die beiden Variablen x und y, weist ihnen Werte zu und zeigt diese Werte dann in einer Hinweisbox an. Module Module1 Sub Main() Dim x, y As Integer x = 3 y = x + 1 MsgBox("x=" & x & " End Sub End Module
y=" & y)
HINWEIS
Per Default müssen in VB.NET Variablen vor ihrer ersten Verwendung deklariert werden. Es ist üblich, alle Variablendeklarationen am Beginn einer Prozedur (hier also am Beginn von Main) durchzuführen, das ist aber keine Bedingung. Auch die elementaren Variablentypen werden intern als Objekte betrachtet. Daher sind die beiden Anweisungen Dim x As Integer und Dim x As New Integer gleichwertig.
Variablendeklaration mit Kennzeichnungszeichen Für die wichtigsten Variablentypen gibt es spezielle Kennzeichner, die eine Kurzschreibweise bei der Deklaration ermöglichen: So können Sie statt Dim x As Integer auch die Kurz-
4.1 Umgang mit Variablen
117
schreibweise Dim x% verwenden. (Ausführlichere Informationen über diese und einige weitere Variablentypen finden Sie im nächsten Abschnitt.) Kennzeichner
Bezeichnung
Platzbedarf
Zahlenbereich
% & @ ! # $
Integer Long Decimal Single Double String
4 Byte 8 Byte 12 Byte 4 Byte 8 Byte 10 + 2*n Byte
-2.147.483.648 bis 2.147.483.647 63 63 -2 bis 2 -1 ±9,99E27 mit 28 Stellen ±3,4E38 mit 8 Stellen ±1,8E308 mit 16 Stellen bis zu 2.147.483.647 Unicode-Zeichen
Die beiden Deklarationsformen können allerdings nicht nach Belieben gemischt werden. Insbesondere dürfen die Kurzschreibweisen nur alleine oder nach As-Anweisungen angegeben werden, nicht aber davor.
HINWEIS
Dim x%, y& Dim b1, b2 As Byte, x%, y& Dim x%, y&, b1, b2 As Byte
'ok 'ok 'Syntaxfehler!
Die VB.NET-Dokumentation rät von der Verwendung von Kennzeichnern bei der Deklaration von Variablen ab. Derartige Kürzel vermindern die Lesbarkeit von Code. Allerdings sparen Sie oft (gerade bei der Deklaration von Parametern) eine Menge Platz, was der Übersichtlichkeit sehr zugute kommt. Letztlich ist es also eine persönliche Entscheidung (oder eine des Entwicklerteams), ob diese Kurzschreibweise angewandt wird oder nicht.
Variablennamen Variablennamen müssen mit einem Buchstaben oder einem Unterstrich beginnen. Die weiteren Zeichen dürfen auch Zahlen enthalten. Es sind auch Sonderzeichen der jeweiligen Sprache erlaubt. Da Programmcode aber per Default mit nur einem Byte pro Zeichen in Dateien gespeichert wird (ANSI), kann die Verwendung von Zeichen außerhalb des USASCII-Zeichensatz Probleme verursachen. Es ist daher eine gute Idee, auf die deutschen Zeichen äöü und ß in Variablennamen zu verzichten. Informationen über die maximale Länge von Zeichenketten habe ich nicht gefunden, Experimente haben aber ergeben, dass diese größer als 256 Zeichen sein darf. Variablennamen sollten nicht mit den Namen von VB.NET-Schlüsselwörtern übereinstimmen. Wenn dies unvermeidlich ist, können Sie den Variablennamen in eckige Klammern stellen. Beispielsweise ist Dim [Next] As Integer zulässig, obwohl Next ein Schlüsselwort zur Formulierung von Schleifen ist.
118
4 Variablen- und Objektverwaltung
Groß- und Kleinschreibung
TIPP
Grundsätzlich spielt in VB.NET die Groß- und Kleinschreibung von Variablen keine Rolle. variable und varIABle und Variable sind also gleichwertig. Der Codeeditor passt die Schreibweise aller Variablen an die Schreibweise an, die bei der Deklaration verwendet wurde. Wenn Sie die Schreibweise einer Variable in der Dim-Anweisung verändern, ändert der Codeeditor nicht automatisch alle anderen Zeilen, in denen die Variable vorkommt. Wenn Sie das möchten, markieren Sie einfach den gesamten Text mit Strg+A und klicken dann Tab an. Damit wird nicht nur der gesamte Code richtig eingerückt, sondern auch die Schreibweise aller Variablen synchronisiert.
Defaultwerte von nicht initialisierten Variablen Neu deklarierte Variablen erhalten automatisch einen Startwert, der aus der folgenden Tabelle hervorgeht. Variablentyp
Startwert
numerische Variablen
0
Boolean-Variablen
False
String-Variablen
Nothing
Bemerkungen
Vorsicht: Nicht initialisierte String-Variablen enthalten tatsächlich Nothing, auch wenn man oft den Eindruck gewinnt, sie enthielten "". Der Grund für diese Missverständnisse ist die VB.NET-Runtime: Diese interpretiert nicht initialisierte String-Variablen wie eine leere Zeichenkette ""! Len(s) liefert daher 0. s="" liefert (eigentlich inkorrekt) True. s Is Nothing und IsNothing(s) liefern korrekt True.
Verwenden Sie die nicht intialisierte String-Variable dagegen im Kontext von .NET-Methoden (z.B. s.Length), tritt ein Fehler auf. Char-Variablen
0
Vorsicht: Die Entwicklungsumgebung, d.h. der Editor, das Kommandofenster und die Überwachungsfenster, zeigen bei nicht initialisierten Char-Variablen Nothing an. Das ist abermals falsch! System.Convert.ToInt16(c) beweist, dass c wirklich ein
Zeichen mit dem Code 0 enthält.
4.1 Umgang mit Variablen
Variablentyp
Startwert
119
Bemerkungen IsNothing(c) funktioniert korrekt und liefert False. c Is Nothing kann für Char-Variablen nicht ausgewertet
werden. Date-Variablen
#12:00:00 AM#
Vorsicht: Diese Zeitangabe entspricht bei deutscher Formatierung 00:00:00 (und nicht etwa 12 Uhr mittags)! VB.NET zeigt diesen Datumswert als reine Zeitangabe an. d.Date ist leer. d.Year, d.Month und d.Day bzw. d.ToString ergeben aber, dass intern das Datum 1.1. des Jahres 0001 gespeichert ist. d.Ticks enthält 0.
Object-Variablen
Nothing
Objektvariablen werden im nächsten Abschnitt beschrieben.
Wertzuweisung (Initialisierung) bei der Deklaration Variablen können unmittelbar bei der Deklaration initialisiert werden. Die erforderliche Syntax sieht so aus: Dim i1 As Integer = 1 Dim i2 As Integer = 2, i3 As Integer = 3 Dim i4% = 4, i5% = 5
Manche Datentypen und alle gewöhnlichen Klassen (Details folgen im nächsten Abschnitt) sehen zur Initialisierung von Variablen den New-Operator vor.
VERWEIS
Dim d As New Date(2001, 12, 31) Dim s1 As New String("a", 3)
'd1 enthält das Datum 31.12.2001 's1 enthält "aaa"
Was passiert, wenn Sie a=b ausführen? Wird in a eine Kopie von b gespeichert oder ein Verweis auf b? Bevor diese Frage beantwortet werden kann, müssen Sie zunächst Objektvariablen kennen lernen. Die Antwort folgt dann in Abschnitt 4.1.3.
4.1.2
Objektvariablen
Was sind Objekte? Ein Kapitel zur Variablenverwaltung ohne die Berücksichtigung von Objekten wäre unvollständig. Allerdings werden Objekte und die Grundzüge objektorientierter Programmierung erst in den folgenden Kapiteln ausführlich beschrieben. Daher ist hier ein Vorgriff unvermeidbar. (Wenn die Beschreibung der Nomenklatur an dieser Stelle zu schnell geht, muss ich Sie auf die nachfolgenden Kapitel vertrösten.)
120
4 Variablen- und Objektverwaltung
Klasse (Typ): Eine Klasse beschreibt die Eigenschaften eines Objekts. Die Klasse ist gewissermaßen der Bauplan für ein Objekt. In der .NET-Klassenbibliothek sind mehrere Tausend Klassen definiert, die bei der Organisation und Verwaltung aller möglichen Dinge helfen: Es gibt Klassen zur Beschreibung der Eigenschaften einer Datei (z.B. System.IO.FileInfo), Klassen zur Darstellung eines Fensters (z.B. System.Windows.Forms.Form), Klassen für alle Steuerelemente, die im Fenster angezeigt werden (z.B. System.Windows.Forms.Button), Klassen zur Erzeugung von Zufallszahlen (System.Random) etc. Darüber hinaus können Sie selbst eigene Klassen definieren. Statt Klasse wird manchmal auch der Begriff Typ verwendet, vor allem in der Zusammensetzung Werttype bzw. Referenztyp. Gemeint sind damit Klassen mit bestimmten Merkmalen – siehe unten. Vererbung: Klassen können ihre Eigenschaften und Methoden an andere Klassen gewissermaßen vererben. Das bedeutet, dass mehrere von einer Basisklasse abgeleitete Klassen dieselben Grundeigenschaften und -methoden haben. Vererbung ist ein wichtiges Werkzeug bei der Programmierung eigener Klassen. Objekte: Objekte sind konkrete Realisierungen von Klassen. In Variablen speichern Sie daher Objekte, nicht Klassen. Wenn die folgende Zeile ausgeführt wird, dann enthält die Variable myRandomObjekt ein Objekt der Klasse System.Random. Dim myRandomObject As New System.Random()
Werttypen (ValueType) versus Referenztypen Viele Programmiersprachen differenzieren zwischen gewöhnlichen Variablen (z.B. für Integer-Zahlen) und Objektvariablen. Nicht so VB.NET: Hier ist jede Variable eine Objektvariable! Wenn hier und in vielen anderen Büchern dennoch manchmal zwischen Variablen und Objektvariablen differenziert wird, dann bezieht sich diese Unterscheidung auf den Typ der zugrunde liegenden Klasse. Die .NET-Bibliothek teilt nämlich alle Klassen in zwei ganz wesentliche Typen ein: •
Werttypen: Zu dieser Gruppe zählen unter anderem alle elementaren Datentypen (z.B. Integer, Double etc.) mit der Ausnahme von String. Das entscheidende interne Merkmal besteht darin, dass sie von der Klasse ValueType abgeleitet sind. (Daraus resultiert auch die Bezeichnung ValueType-Klassen bzw. ValueType-Objekte.) Neben den elementaren Datentypen gibt es eine ganze Reihe von Klassen und Datenstrukturen in der .NET-Bibliothek, die ebenfalls von ValueType abgeleitet sind. Zumeist handelt es sich dabei um eher kleine Klassen (klein hinsichtlich des Speicherbedarfs, aber auch klein hinsichtlich der angebotenen Funktionen). Zwei Beispiele sind System.Drawing.Rectangle und System.Drawing.Color. Auch die in VB.NET definierten Strukturen (Structure ... End Structure) und Aufzählungen (Enum) werden von ValueType abgeleitet.
4.1 Umgang mit Variablen
•
121
Referenztypen: Zu dieser Gruppe zählen alle Klassen, die nicht von ValueType abgeleitet sind. Die Bezeichnung Referenztypen (im Englischen reference types oder reference classes) resultiert daraus, dass Objekte dieser Klassen als Referenz (als Zeiger) in Variablen gespeichert bzw. an Prozeduren oder Methoden übergeben werden. Zu den Referenztypen zählt die Mehrheit aller Klassen der .NET-Bibliothek, z.B. System.Array, System.IO.FileInfo, System.Drawing.Bitmap und System.Windows.Forms.Button, um willkürlich vier Beispiele herauszugreifen. ValueType-Klassen sind so gesehen nur eine (sehr wichtige) Ausnahme. Beachten Sie, dass auch Felder (die intern durch die Klasse System.Array verwaltet werden) zu den Referenztypen zählen – selbst dann, wenn deren Elemente oft Werttypen sind (z.B. Integer-Zahlen).
Die Motivation für die Trennung zwischen Wert- und Referenztypen lautet kurz und einfach: Effizienz. Damit der objektorientierte Ansatz von .NET konsequent verfolgt werden kann, müssen alle Daten Objekte sein. Durch die Behandlung als Objekte entsteht aber ein hoher Overhead, der bei einfachen Daten (z.B. bei Integer-Zahlen) in keinem Verhältnis zum Nutzen stehen würde. Deswegen muss es für den Compiler einen Weg geben, mit manchen Objekten (eben mit den ValueType-Objekten) so umzugehen, als wären es nur einfache Werte. Objekte, die von Wert- bzw. von Referenztypen abgeleitet sind, unterscheiden sich in ihrer Verwendung ganz erheblich: bei Variablenzuweisungen, bei der Übergabe als Parameter an eine Prozedur oder Methode, bei der internen Speicherverwaltung etc. In diesem und in den folgenden Kapiteln wird immer wieder auf diesen Unterschied hingewiesen. Daher ist es wichtig, dass Sie zwischen Wert- und Referenztypen differenzieren können! Wie können Sie aber feststellen, welchem Typ eine bestimmte Klasse angehört? Am besten verwenden Sie dazu den Objektbrowser, den Sie mit ANSICHT|ANDERE FENSTER öffnen. Dort suchen Sie die Klasse, die Sie interessiert, und klicken dann das Pluszeichen um, um die übergeordneten Klassen zu ermitteln (also die Klassen, die durch Vererbung implementiert sind). Abbildung 4.1 zeigt, dass die Klasse System.Drawing.Color von der ValueTypeKlasse abgeleitet ist. Damit ist klar, dass Color ein Werttyp ist.
Abbildung 4.1: Die Klasse System.Drawing.Color ist eine ValueType-Klasse
VERWEIS
122
4 Variablen- und Objektverwaltung
Ein übersichtliches Schema der .NET-Datentypen samt der Unterscheidung zwischen Wert- und Referenztypen finden Sie in der Online-Dokumentation, wenn Sie nach Übersicht Typensystem suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpconcommontypesystemoverview.htm
Deklaration von Objektvariablen Wenn Sie eine Variable deklarieren, die auf ein bestimmtes Objekt verweisen soll, ändert sich an der Syntax nichts: Statt des Datentyps geben Sie jetzt den Klassennamen ein. Dim myRandomObject As System.Random
Die Variable myRandomObject ist damit allerdings noch leer! Bevor Sie Methoden der System.Random-Klasse nutzen können, müssen Sie das Objekt erst erzeugen. myRandomObject = New System.Random()
Im Regelfall werden Sie diese beiden Anweisungen mit Dim As New in einer einzigen Zeile vereinen: Dim myRandomObject As New System.Random()
Noch kompakter wird die Zeile, wenn Sie auf die Angabe von System verzichten (was im Regelfall möglich ist – mehr dazu in Abschnitt 6.2.) Dim myRandomObject As New Random()
Bei vielen Klassentypen können an die New-Methode Parameter übergeben werden. Die folgende Anweisung erzeugt ein Objekt der Klasse System.IO.DirectoryInfo. Das Objekt wird gleich mit dem Pfad des aktuellen Verzeichnis initialisiert.
VERWEIS
Dim dir As IO.DirectoryInfo = New IO.DirectoryInfo(".")
Elementare Variablentypen wie Integer, String sind so genannte Werttypen und stellen einen Sonderfall dar. Zwar betrachtet VB.NET auch solche Variablen intern als Objekte, dennoch können diese Variablen sofort nach der Deklaration verwendet werden. Das Schlüsselwort New ist zwar erlaubt (und manchmal zur Initialisierung auch nützlich – siehe unten), es ist aber nicht erforderlich. Hintergrundinformationen über die Interna der Variablenverwaltung und insbesondere über den Unterschied zwischen Wert- und Referenztypen finden Sie in Abschnitt 4.6.
Objektvariablen ohne Typangabe Wenn Sie im Vorhinein nicht wissen, wie Sie eine Objektvariable verwenden werden, können Sie die Variable sehr allgemein als Object deklarieren. Sie können diese Variable dann im weiteren Verlauf nach Belieben verwenden. Durch myObject=3 wird in myObject ein Inte-
4.1 Umgang mit Variablen
123
ger-Wert gespeichert. Durch New System.Random wird ein Objekt des Typs System.Random gespeichert etc. Dim myObject As Object myObject = 3 myObject = New System.Random()
'myObject verweist jetzt auf eine 'Integer-Variable 'myObject verweist jetzt auf ein 'Objekt der Klasse System.Random
Diese Vorgehensweise ist allerdings mit Nachteilen verbunden: Erstens weiß die Entwicklungsumgebung nicht, wie Sie die Variable verwenden werden, und kann daher bei der Codeeingabe nicht die für ein Objekt relevanten Methoden und Eigenschaften vorschlagen. Zweitens weiß auch der Compiler nicht, wie Sie die Variable verwenden, und kann daher nur eine eingeschränkte Syntaxkontrolle durchführen. Drittens kann es passieren, dass Sie der Variable versehentlich einen Wert zuweisen und so unbeabsichtigt den Objekttyp verändern. (Dabei kommt es zu keinem Fehler. Probleme kann es aber später geben, wenn Sie Objekteigenschaften oder -methoden einsetzen, die es für den neuen Objekttyp gar nicht gibt.) Und zu guter Letzt ist der Code etwas langsamer – aber dieses Argument ist wahrscheinlich das unwichtigste.
VERWEIS
Fazit: Geben Sie bei der Deklaration nach Möglichkeit exakt den Daten- oder Objekttyp an! Sie ersparen sich damit eine oft langwierige Fehlersuche. (Lesen Sie auch den Abschnitt zu Option Explicit und Option Strict etwas weiter unten!) In Abschnitt 4.6.4 werden verschiedene Wege vorgestellt, mit denen Sie den Typ einen Variable bestimmen können. Am vielseitigsten ist dabei die Methode GetType.
4.1.3
Variablenzuweisungen
VERWEIS
Was passiert, wenn Sie a = b ausführen? Diese Frage klingt trivial, aber wenn es wirklich so trivial wäre, gäbe es dazu natürlich keinen eigenen Abschnitt. Das Ergebnis einer Variablenzuweisung hängt nämlich von der Art der Variablen ab. Variablen zur Speicherung von Werttypen verhalten sich anders als solche für Referenztypen. Dieser Abschnitt setzt voraus, dass a und b für denselben Daten- bzw. Objekttyp deklariert wurden. Wenn das nicht der Fall ist, kommt es bei der Zuweisung zu einer automatischen Konvertierung der Daten. Diese gelingt allerdings nur in Sonderfällen (z.B. wenn a eine Double- und b eine Integer-Variable ist). Wenn eine verlustfreie Typumwandlung dagegen nicht möglich ist, kommt es zu einer Fehlermeldung. Mehr Informationen zum Thema der automatischen und manuellen TypenKonvertierung erhalten Sie in Abschnitt 8.4.
124
4 Variablen- und Objektverwaltung
Werttypen (ValueType-Variablen) Bei Werttypen, d.h. bei den meisten elementaren Datentypen (Integer, Double, Date etc.), wird durch a = b eine Kopie der Daten erstellt und zugewiesen. Dim Dim a = b =
a As Integer = 1 b As Integer = 2 b 'jetzt ist a=2 und b=2 3 'jetzt ist a=2 und b=3
Referenztypen (herkömmliche Objektvariablen) Bei Referenztypen (deren Klasse nicht von ValueType abgeleitet ist) wird durch a = b dagegen ein Link (eine Referenz) auf das Objekt zugewiesen, also keine Kopie! Im folgenden Beispiel wird in a und b jeweils ein Button-Objekt gespeichert. Derartige Objekte dienen normalerweise zur Darstellung von Buttons in einem Fenster. Die Zuweisung a = b bewirkt hier, dass nun beide Variablen auf denselben Button verweisen. Durch die Veränderung der Eigenschaft b.Text ändert sich nun auch a.Text (weil ja a und b auf dasselbe Objekt verweisen)! Dim a As Dim b As a.Text = b.Text = a = b b.Text =
New Windows.Forms.Button() New Windows.Forms.Button() "a" "b" 'a und b zeigen jetzt auf denselben Button (.Text="b") "x" 'damit wird .Text für a und für b verändert!
VERWEIS
Wenn Sie bei gewöhnlichen Objekten eine Kopie der Daten benötigen, müssen Sie a = b.Clone() ausführen. Allerdings steht die Methode Clone nicht für alle Klassen zur Verfügung. Wenn es Clone nicht gibt, besteht keine Möglichkeit, ein Objekt zu kopieren. (Die Button-Klasse sieht kein Clone vor.) Vielleicht fragen Sie sich, was mit dem ursprünglich für a erzeugten Button passiert: Es gibt nun ja keine Variable mehr, die darauf verweist. Deswegen wird das Objekt nach einiger Zeit automatisch aus dem Speicher entfernt. Dieser Prozess wird garbage collection genannt und ist in Abschnitt 4.6.2 beschrieben.
String-Variablen String-Variablen zur Speicherung von Zeichenketten stellen einen Sonderfall dar. Zwar zählt String zu den elementaren Datentypen, es handelt sich aber intern dennoch um einen Referenztyp. (Die String-Klasse ist nicht von ValueType abgeleitet.) Und trotzdem verhalten sich String-Variablen wie Werttypen!
4.1 Umgang mit Variablen
125
Das hat folgenden Grund: String-Objekte gelten als unveränderlich (immutable). Das bedeutet, dass bei jeder Veränderung einer Zeichenkette ein vollkommen neues String-Objekt erzeugt wird. Aus diesem Grund betrifft eine Änderung immer nur eine Variable (auch wenn vorher zwei Variablen auf dasselbe String-Objekt verwiesen haben). Dim Dim a = b =
a As b As b "x"
String = "a" String = "b" 'a und b verweisen nun auf "b" 'a verweist weiterhin auf "b"; 'b verweist auf das neue String-Objekt "x"
Geradezu verblüffend ist das Verhalten, wenn Sie nun auch a die Zeichenkette "x" zuweisen: Dann verweisen a und b wieder auf dasselbe String-Objekt (d.h., der Objektvergleich a Is b liefert True). a = "x"
'a und b verweisen nun auf dasselbe String-Objekt "x"
Dieses Verhalten gilt allerdings nicht immer. Wenn Sie zuerst a="12" und dann b="1" und b+="2" ausführen, dann enthalten zwar a und b dieselbe Zeichenkette "12", aber intern verweisen die beiden Variablen auf unterschiedliche Objekte (weil der Compiler hier nicht erkennen konnte, dass die Ergebnisse der Zuweisungen gleichartige Objekte sein würden).
4.1.4
Option Explicit und Option Strict
Option Explicit: In VB.DOT müssen per Default alle Variablen mit Dim deklariert werden, bevor sie verwendet werden können. Der Grund für dieses Verhalten ist die projektweite Defaulteinstellung für Option Explicit. Falls Sie diese Einstellung verändern möchten (dazu gibt es aber keinen vernünftigen Grund!), markieren Sie im PROJEKTMAPPEN-EXPLORER Ihr Projekt und stellen Sie dann in PROJEKT|EIGENSCHAFTEN|ALLGEMEINE EIGENSCHAFTEN|ERSTELLEN das Listenfeld OPTION EXPLICIT auf AUS. Sie können Option Explicit auch getrennt für einzelne Codedateien einstellen: Option Explicit On|Off Option Strict: Diese Option hat auf ersten Blick nichts mit der Deklaration von Variablen zu tun, sondern damit, ob VB.NET (wie VB6) versucht, Umwandlungen zwischen verschiedenen Datentypen automatisch durchzuführen. Dies ist in der Defaulteinstellung der Fall. Wenn Sie Option Strict aktivieren (entweder projektweit mit PROJEKT|EIGENSCHAFTEN oder für eine einzelne Codedatei mit Option Strict On), müssen Sie bei allen Umwandlungen, die das Risiko eines Datenverlusts in sich bergen, explizit eine Konvertierungsfunktion angeben. (Weitere Information zur Konvertierung zwischen Datentypen finden Sie in Abschnitt 8.4.)
Gleichsam eine Nebenwirkung von Option Strict On besteht darin, dass Dim x ohne explizite Typenangabe nicht mehr erlaubt ist. Sie müssen bei jeder Variablen den gewünschten Typ angeben (wobei As Object natürlich weiterhin zulässig bleibt).
TIPP
4 Variablen- und Objektverwaltung
Wenn Sie Wert auf exakten Code legen, sollten Sie generell beide Optionen, also auch Option Strict, aktivieren. Sie ersparen sich dadurch eine Menge Zeit bei der Suche von Fehlern, die durch Tippfehler (Option Explicit) oder durch die unbeabsichtigte Typenkonvertierung (Option Strict) entstehen können.
VERWEIS
126
Die Defaulteinstellung Option Strict Off für neue Projekte kann in der Entwicklungsumgebung nicht verändert werden. (Die Einstellung durch PROJEKT|EIGENSCHAFTEN gilt immer nur für das aktuelle Projekt.) Abschnitt 1.3.7 zeigt aber, wo Sie die Konfigurationsdateien für neue Projekte finden und wie Sie Option Strict dort für alle neuen Projekte eines bestimmten Typs (z.B. für alle Windows-Anwendungen) auf On setzen können.
Option-Strict-Beispiel Die folgenden Zeilen sehen so aus, als würden sie zur aktuellen Zeit in d1 eine Stunde hinzuaddieren. Tatsächlich kommt es bei der Durchführung der Addition aber zu einem Fehler. (In VB6 funktionierte dieser Code tatsächlich. In VB.NET sind derartige Additionen aber nicht zulässig.) Dim d1, d2, d3 As Date d1 = Now d2 = #1:00:00 AM# d3 = d1 + d2 'Fehler in VB.NET!
Was hat nun dieser Code mit Option Strict zu tun? VB.NET betrachtet + hier als einen Operator, um zwei Zeichenketten zu verknüpfen! d1 und d2 werden entsprechend der Ländereinstellung automatisch in Zeichenketten umgewandelt und aneinandergefügt. Der Fehler tritt erst auf, weil die ebenfalls automatische Rückkonvertierung der Zeichenkette in ein Datum für die Zuweisung an d3 scheitert. Die aus d1+d2 resultierende Zeichenkette lautet beispielsweise "29.11.2002 16:23:2101:00:00", wenn der Code am 29.11.2002 um 16:23 im deutschen Sprachraum ausgeführt wurde. (Genau genommen ist die Ländereinstellung des Betriebssystem für das Ergebnis entscheidend.) Wenn Sie Option Strict On verwenden, erkennt die Entwicklungsumgebung sofort, dass hier Probleme auftreten werden. Ohne Option Strict tritt der Fehler dagegen erst bei der tatsächlichen Ausführung des Codes auf.
4.1.5
Syntaxzusammenfassung
Umgang mit Variablen Option Explicit On
bewirkt, dass jede Variable deklariert werden muss. (Diese Einstellung gilt per Default.)
4.2 Variablentypen
127
Umgang mit Variablen Option Strict On
bewirkt, dass nur solche Typenkonvertierungen automatisch durchgeführt werden, die ohne Datenverlust durchgeführt werden können. (Diese Einstellung gilt per Default nicht.)
Dim x As objekttyp [= wert]
deklariert (und initialisiert) eine Variable.
Dim x% [=wert]
Kurzschreibweise für einige Variablentypen.
Dim x As New objekttyp()
deklarierit und initialisiert eine Objektvariable.
x = wert
weist einer Variablen einen Wert zu.
x = New objekttyp()
erzeugt ein neues Objekt der Klasse objekttyp.
x1 = x2
führt eine Variablen- oder Objektzuweisung durch. Wenn x1 und x2 Variablen für Werttypen sind (z.B. Integer), dann wird in x1 eine Kopie von x2 gespeichert. Sind x1 und x2 dagegen Objektvariablen für Referenztypen, enthält x1 nach der Zuweisung einen Verweis auf den Inhalt von x2.
4.2
Variablentypen
VERWEIS
Dieser Abschnitt gibt einen Überblick über die elementaren Variablentypen (Datentypen) von VB.NET. Genau genommen handelt es sich dabei um .NET-Klassen wie System.Boolean, System.Byte etc., die unter VB.NET aber zum Teil unter andern Namen verwendet werden können. (Beispielsweise lautet die VB.NET-Bezeichnung für System.Int16 einfach Short.) Dieser Abschnitt stellt die elementaren Datentypen nur kurz vor. Eine ausführliche Beschreibung der vielen Methoden, die es zur Bearbeitung, Formatierung und Konvertiertung von Zahlen, Daten und Zeichenketten gibt, finden Sie in Kapitel 8.
4.2.1
Ganze Zahlen (Byte, Short, Integer, Long)
Die folgende Tabelle fasst die in VB.NET vorgesehenen Datentypen zur Speicherung ganzer Zahlen zusammen. Die in der ersten Spalte angegebenen Zeichen können als Kurzschreibweise zur Kennzeichnung des Datentyps verwendet werden. Die beiden folgenden Deklarationen sind deswegen gleichwertig: Dim i As Integer Dim i%
128
4 Variablen- und Objektverwaltung
Bezeichnung
.NET-Bezeichnung
Platzbedarf
Zahlenbereich
Boolean
System.Boolean
1 Byte
True oder False
Byte
System.Byte
1 Byte
0 bis 255
Short
2 Byte
-32.768 bis 32.767
System.Int32
4 Byte
-2.147.483.648 bis 2.147.483.647
&
System.Int64
8 Byte
-2 bis 2 -1
VERWEIS
System.Int16
% Integer Long
63
63
Bitte beachten Sie, dass sich die Angaben für den Platzbedarf auf die eigentlichen Daten beziehen. Je nach Anwendung (z.B. wenn die Daten in einer als Object deklarierten Variable gespeichert werden) kann der tatsächliche Platzbedarf deutlich größer sein. Diese Interna der Variablenverwaltung werden in Abschnitt 4.6 beschrieben.
4.2.2
Fließ- und Festkommazahlen (Single, Double, Decimal)
VORSICHT
Der Defaultdatentyp für Fließkommazahlen und Fließkommaberechnungen ist Double. Dieser Datentyp kann auch intern am effizientesten verarbeitet werden. Single sollte nur dann eingesetzt werden, wenn der Platzbedarf eine wichtige Rolle spielt (etwa bei riesigen Feldern). Die Division x = 1.0 / 0.0 löst (anders als bei ganzen Zahlen) keinen Fehler aus! Stattdessen ist das Resultat einer derartigen Division der Wert Double.NegativeInfinity oder Double.PositiveInfinity. Einige weitere Informationen zu diesem interessanten Aspekt von Single- und Double-Zahlen sind in Abschnitt 8.1.3 beschrieben.
Der Datentyp Decimal eignet sich insbesondere zur Speicherung von Werten, bei denen keine Rundungsfehler auftreten dürfen. Decimal-Variablen sind daher besonders gut geeignet, wenn Geldbeträge verarbeitet werden sollen. Die Genauigkeit beträgt 28 Stellen, wobei die Position des Kommas variabel ist. (Wenn eine Zahl 10 Stellen vor dem Komma beansprucht, stehen für den Nachkommaanteil noch 18 Stellen zur Verfügung.) Decimal-Variablen sind aber auch mit Nachteilen verbunden: Rechenoperationen werden viel langsamer als mit Double-Variablen ausgeführt, der Speicherbedarf ist größer, und anders als bei Single und Double ist keine Exponentialdarstellung zulässig. Deswegen ist
der zulässige Zahlenbereich viel kleiner. Der bis VB6 zur Verfügung stehende Datentyp Currency wird von VB.NET nicht mehr unterstützt (verwenden Sie Decimal!).
4.2 Variablentypen
129
Bezeichnung
.NET-Bezeichnung
Platzbedarf
Zahlenbereich
@
Decimal
System.Decimal
12 Byte
±9,99E27 mit 28 Stellen
#
Double
System.Double
8 Byte
±1,8E308 mit 16 Stellen
!
Single
System.Single
4 Byte
±3,4E38 mit 8 Stellen
4.2.3
Datum und Uhrzeit (Date)
Bezeichnung
.NET-Bezeichnung
Platzbedarf
Zeitbereich
Date
System.DateTime
8 Byte
1.1.0001 00:00:00 bis 31.12.9999 23:59:59
Anders als in VB6, wo Daten und Zeiten als Double-Wert gespeichert werden, erfolgt die interne Repräsentierung nun durch einen Long-Wert. Dieser Wert gibt die Anzahl so genannter Ticks zu je 100 ns (Nanosekunden) an, die seit dem 1.1.0001 00:00 vergangen sind. Mit datevar.Ticks können Sie diesen internen Wert auslesen. System.DateTime sieht eigentlich einen Zeitbereich vom 1.1.0001 bis zum 31.12.9999 vor. Die VB.DOT-Dokumentation spricht hingegen vom 1.1.100 als kleinstmöglichem Datum. Diese Aussage resultiert allerdings nicht aus den internen Möglichkeiten von DateTime, sondern aus der Art und Weise, wie VB.DOT mit zweistelligen Jahrenszahlen umgeht. Wenn Sie ein zweistelliges Datum zuweisen (z.B. datevar=#12/31/15#), wird die Jahreszahl automatisch in 1930 bis 2029 umgewandelt (beim gewählten Beispiel also 2015). Ein Sonderfall ist die Zuweisung einer Uhrzeit ohne Datumsangabe: In diesem Fall verwendet auch VB als Datum dem 1.1.0001.
4.2.4
Zeichenketten (String)
HINWEIS
Zeichenketten werden intern generell im Unicode-Format gespeichert (also mit zwei Byte pro Zeichen). Das ermöglicht die Speicherung fast aller ausländischer Sonderzeichen (auch für asiatische Zeichensätze, die mehr als 256 Zeichen umfassen). Per Default speichert die Entwicklungsumgebung VB.NET-Code in ANSI-Dateien (ein Byte pro Zeichen, Codierung entsprechend der Codeseite 1252). Wenn Sie im Code Unicode-Zeichen verwenden möchten, die sich außerhalb dieser Codeseite befinden, müssen Sie den Programmcode im Unicode-Format abspeichern. Dazu führen Sie DATEI|ERWEITERTE SPEICHEROPTIONEN aus und wählen einen der zur Auswahl stehenden Unicodes aus.
130
4 Variablen- und Objektverwaltung
Unverständlich ist in diesem Zusammenhang, dass VB.NET-Code weiterhin in ANSI-Dateien (ein Byte pro Zeichen) gespeichert wird. Mit anderen Worten: VB.NET-Programme kommen zwar intern gut mit Unicode zurecht, aber im Programmcode sind Sie auf einen Ein-Byte-Zeichensatz beschränkt. Daher ist es unmöglich, in Ihrem Code einer Zeichenkette beliebige Unicode-Zeichen zuzuweisen. Ebenso kann es Probleme geben, wenn Programmcode zwischen Ländern mit einem unterschiedlichen Zeichensatz ausgetauscht wird. Eine weitere Besonderheit bei Zeichenketten besteht darin, dass es sich hierbei um unveränderliche (immutable) Objekte handelt. Wenn Sie x = x + "abc" ausführen, wird eine neue Zeichenkette gebildet und die alte Zeichenkette verworfen. (Die alte Zeichenkette wird automatisch aus dem Speicher entfernt.) Das gilt selbst dann, wenn sich die Länge der Zeichenkette nicht ändert oder wenn sich die Zeichenkette verkürzt.
HINWEIS
$
Bezeichnung
.NET-Bezeichnung
Platzbedarf
Inhalt
Char
System.Char
2 Byte
ein Unicode-Zeichen
String
System.String
10 + 2*n Byte bis zu 2.147.483.647 Unicode-Zeichen
Wenn Char-Variablen leer sind, haben sie den Wert Chr(0). Leere String-Variablen liefern im Gegensatz dazu eine leere Zeichenkette!
4.2.5
Objekte
Wenn Sie mit Dim eine Variable deklarieren, ohne explizit einen Typ anzugeben (das ist nur möglich, wenn Option Strict Off gilt), verwendet VB.NET Object als Defaulttyp. In ObjectVariablen können beliebige Daten gespeichert werden – Zahlen, Zeichenketten, Daten und Zeiten sowie Objekte aller .NET-Klassen. Object ist daher der allgemeingültigste Variablentyp von Visual Basic. Wenn Sie sich keine Gedanken über den richtigen Datentyp machen möchten und mit Option Strict Off arbeiten (siehe Abschnitt 4.1.4), können Sie daher immer Object-Variablen verwenden. Visual Basic kümmert sich nun selbst darum, intern den richtigen Variablentyp einzusetzen. Der Preis dieser Bequemlichkeit ist aber hoch: Ihr Programm wird sowohl ineffizient als auch fehleranfällig sein (d.h., Sie werden viele Fehler erst bemerken, wenn Sie das Programm tatsächlich ausführen).
Obwohl Object auf den ersten Blick ähnliche Eigenschaften wie der aus VB6 vertraute Variablentyp Variant hat, ist er intern ganz anders realisiert. Es handelt sich dabei um die Überklasse (System.Object), von der alle anderen Variablentypen (und generell alle .NET-Klassen) abgeleitet sind. Die Object-Variable besteht im Wesentlichen aus einem Zeiger (pointer) auf die tatsächlichen Daten. Insofern beträgt der Speicherbedarf für eine noch nicht initialisierte Object-Variable nur vier Byte. Sobald die Variable tatsächlich benutzt wird, kommt dazu aber noch der Speicherbedarf für die tatsächlichen Daten.
4.2 Variablentypen
131
Bezeichnung
.NET-Bezeichnung
Platzbedarf
Object
System.Object
4 Byte für den Zeiger (plus n Byte für die Daten)
In diesem Buch werden Sie kaum auf Object-Variablen stoßen. Stattdessen sind Objektvariablen fast immer exakt deklariert. Die folgende Anweisung deklariert dir beispielsweise als Variable, in der ein Objekt der Klasse System.IO.DirectoryInfo gespeichert werden kann. Dim dir As IO.DirectoryInfo
4.2.6
Weitere .NET-Datentypen
Bei den von VB.NET direkt unterstützten Datentypen handelt es sich um die so genannten CLS-Datentypen. CLS steht für Common Language Specification und beschreibt einen Standard für .NET-Bibliotheken. Eine CLS-konforme Bibliothek hat den Vorteil, dass sie von allen .NET-Programmiersprachen genutzt werden kann. Das setzt unter anderem voraus, dass die Schnittstelle dieser Bibliothek nur die CLS-Datentypen verwendet. Neben den CLS-Datentypen kennt .NET aber eine Reihe weiterer Datentypen. Sie finden diese Datentypen in der System-Klasse der mscorlib-Bibliothek. (Diese Bibliothek steht in allen VB.NET-Programmen immer zur Verfügung.) Die folgende Tabelle zählt lediglich die interessantesten Datentypen auf. .NET-Datentypen, die von VB.NET nicht direkt unterstützt werden System.GUID
128-Bit-Integerzahlen zur Speicherung von globally unique identifier
System.SByte
8-Bit-Integerzahlen mit Vorzeichen
System.TimeSpan
Zeitspannen (relative Zeitangaben, siehe Abschnitt 8.3.1)
System.UInt16, .UInt32 und .UInt64 16-, 32- oder 64-Bit-Integerzahlen ohne Vorzeichen
Wie das folgende Beispiel beweist, erlaubt VB.NET durchaus die Deklaration derartiger Variablen: Dim i1, i2 As System.UInt32
Auch eine gegenseitige Zuweisung (i1=i2) ist möglich. Aber sobald Sie versuchen, den Variablen einen Wert zuzuweisen (i1=3), gibt es Probleme. VB beklagt sich, dass es den Datentyp Integer nicht in System.UInt32 konvertieren kann. Dieses Problem kann noch relativ leicht umgangen werden: Mit der Funktion System.Convert.ToUInt32 können die meisten Datentypen zu UInt32 konvertiert werden. (Natürlich gibt es auch äquivalente Funktionen für UInt16 und UInt64.) i1 = Convert.ToUInt32(100)
132
4 Variablen- und Objektverwaltung
Wenn Sie nun als Nächstes versuchen, eine einfache arithmetische Operation durchzuführen (z.B. i2=i1-1), gibt es neuerlich Probleme: der Minusoperator von VB ist nicht in der Lage, einen UInt32-Wert (den Inhalt von i1) und einen Int32-Wert (1) miteinander zu verknüpfen. Zwar können Sie auch dieses Problem umgehen, aber Sie erkennen jetzt sicher, dass die Verwendung von Datentypen, die VB nicht explizit unterstützt, wenig Freude bereitet. (Ganz abgesehen davon liefert diese Subtraktion nur dann korrekte Ergebnisse wenn i1 innerhalb des Int32-Zahlenbereichs liegt – Sie haben also im Vergleich zu gewöhnlichen Integer-Variablen nichts gewonnen.)
HINWEIS
i2 = Convert.ToUInt32(System.Convert.ToInt32(i1) - 1)
C# kann im Gegensatz zu VB.NET problemlos mit UIntxx-Variablen umgehen. Gerüchten zufolge soll VB.NET das in der nächsten Version auch können.
Auch die Anwendung der anderen Datentypen aus der obigen Tabelle ist prinzipiell möglich, in der Praxis aber mit ähnlich hohem Konvertierungsaufwand verbunden. Beispiele für die Verwendung von TimeSpan-Variablen finden Sie auch in Abschnitt 8.3.1, wo es um den Umgang mit Daten und Zeiten geht.
4.3
Konstanten
Konstanten selbst definieren Mit Const können Sie Konstanten deklarieren. Die Syntax von Const entspricht weitgehend der von Dim. Der Unterschied besteht darin, dass die so deklarierten Konstanten unveränderlich sind. Const const1 As Integer = 3, const2 As Double = 4.56
Eine merkwürdige Eigenheit von selbst definierten Konstanten besteht darin, dass Sie beim Testen von Programmen in der Entwicklungsumgebung nicht sichtbar sind. Konstanten können weder im Befehlsfenster verwendet noch in Überwachungsfenstern angezeigt werden.
Vordefinierte VB-Konstante In VB.NET stehen zahllose vordefinierte Konstanten zur Verfügung. Die folgende Tabelle ist alles andere als komplett. Sie soll lediglich als erste Orientierungshilfe dienen.
4.3 Konstanten
133
Konstante
Verwendung
Definiert in
True=-1, False=0
enthalten Wahrheitswerte.
VB.NET-Sprachdefinition
Nothing
gibt an, dass die Variable nicht initialisiert (also leer) ist.
VB.NET-Sprachdefinition
vbXxx (z.B. vbCrLF, vbTab, vbYes etc.)
erhöhen die Kompatibilität mit VB6.
Microsoft.VisualBasic.Constants
Back, Cr, CrLf, FormFeed, Lf, NewLine, NullChar, Quote, Tab, VerticalTab
dienen zur Zusammensetzung von Zeichenketten (siehe Abschnitt 8.2).
Microsoft.VisualBasic.ControlChars
(Teil der VB.NET-Runtime)
(Teil der VB.NET-Runtime)
Beachten Sie, dass viele Definitionen doppelgleisig sind: sowohl ControlChars.Tab als auch vbTab enthalten den Code 9 (ein Tabulatorzeichen), sowohl MsgBoxResult.Abort als auch vbAbort enthalten den Wert 3 etc. Es ist eine Geschmacksfrage, welche Konstanten Sie verwenden.
VERWEIS
Der Vorteil der vbXxx-Konstanten besteht darin, dass diese in VB-Programmen unmittelbar verwendet werden können. Bei allen anderen Konstanten muss jeweils die dazugehörende Klasse angegeben werden (ControlChars für Char-Konstanten, MsgBoxResult und MsgBoxStyle für Konstanten zum Aufruf und zur Auswertung von MsgBox etc.) Eine Beschreibung aller Konstanten in Microsoft.VisualBasic finden Sie, wenn Sie in der Hilfe nach Konstanten Enumerationen suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/vblr7/html/vaoriconstvba.htm
.NET-Konstanten Neben den hier beschriebenen VB-spezifischen Konstanten gibt es in den .NET-Klassenbibliotheken natürlich unzählige weitere Konstanten. Diese sind sehr oft als so genannte Enum-Aufzählungen realisiert (siehe den folgenden Abschnitt), was ihre Anwendung erleichtert. Generell müssen Sie bei der Verwendung solcher Konstanten den gesamten Klassennamen angeben, also z.B. IO.FileAttributes.Compressed für die Konstante Compressed (die eines der vielen möglichen Dateiattribute beschreibt).
134
4 Variablen- und Objektverwaltung
HINWEIS
4.4
Enum-Aufzählungen Beachten Sie bitte, dass der Begriff Aufzählung zweideutig ist. Er wird einerseits dazu verwendet, um die hier beschriebenen Enum-Konstrukte zu benennen. (Das sind Gruppen von Konstanten.) Andererseits meint Aufzählung oft auch ein Objekt des System.Collections-Namensraums. Damit können Sie Listen verwalten, in die Sie jederzeit Elemente einfügen und wieder löschen können. Diese Art von Aufzählungen werden in Kapitel 9 beschrieben.
4.4.1
Syntax und Anwendung
Mit der Anweisung Enum name – End Enum können Sie eine ganze Gruppe von Konstanten definieren. Die folgenden Zeilen demonstrieren die Syntax: Enum myColors As Integer Red 'automatisch Wert 0 Green 'automatisch Wert 1 Blue = 10 'Wert 10 Yellow 'automatisch Wert 11 End Enum mycolors gilt nun wie ein neuer Datentyp. Sie können also eine Variable vom Typ mycolors deklarieren: Dim col As myColors
Wenn Sie mit Option Strict arbeiten, können Sie dieser Variable ausschließlich die im EnumBlock definierten Konstanten zuweisen: col = myColors.Green col = 0
'OK 'nicht erlaubt, falls Option Strict On
Auch bei Vergleichen können Sie ausschließlich die Enum-Konstanten verwenden: If col = myColors.Red Then ... End If
Der Vorteil eines Enum-Blocks besteht also darin, dass die so definierten Konstanten nur im Kontext von entsprechenden Enum-Variablen verwendet werden können. Das erleichtert die Codeeingabe (der Editor schlägt automatisch die zulässigen Konstanten vor) und vermindert die Gefahr durch die versehentliche Verwendung von (für die jeweilige Variable oder Datenstruktur) ungeeigneten Konstanten.
4.4 Enum-Aufzählungen
135
Enum-Konstrukte können innerhalb von Modulen, Klassen und Strukturen sowie auf
äußerster Ebene im Code (also außerhalb anderer Konstrukte) definiert werden. Es ist aber nicht möglich, eine Enum-Aufzählung innerhalb einer Prozedur zu definieren. Zahlenwert eines Enum-Elementes ermitteln: Nach Möglichkeit sollten Sie im Programmcode im Umgang mit Enum-Aufzählungen ausschließlich die definierten Konstanten verwenden (für Zuweisungen, Vergleiche etc.). Sollte Sie aus irgendeinem Grund dennoch den Zahlenwert benötigen, können Sie diesen jederzeit mit den VB-Konvertierungsfunktionen ermitteln (also etwa CInt(col) oder CInt(myColors.Red)). Enum-Datentyp: Als Datentyp für die Konstanten kommen Byte, Short, Integer oder Long in Frage. Laut Dokumentation muss der Datentyp in der ersten Zeile des Enum-Blocks angegeben werden, wenn Option Strict verwendet wird. Tatsächlich ist die Angabe des Datentyps aber anscheinend immer optional, wobei Integer als Defaultdatentyp gilt.
Automatische Werte: Sie können jedem Enum-Element einen beliebigen (ganzzahligen) Wert zuweisen. Wenn Sie das nicht tun, verwendet VB.NET per Default 0 für das erste Element, 1 für das zweite etc. Nach den expliziten Wertzuweisungen setzt VB.NET automatisch mit n+1 fort.
HINWEIS
Es ist prinzipiell erlaubt, dass zwei Enum-Elemente denselben Wert enthalten (auch wenn das selten sinnvoll ist). Passen Sie auf, dass das nicht unbeabsichtigt passiert! Da dies syntaktisch erlaubt ist, kommt es zu keinem Fehler. Enum choices As Integer good = 10 'Wert 10 medium 'automatisch Wert 11 bad 'automatisch Wert 12 horrible = 12 'ebenfalls 12! End Enum
4.4.2
Enum-Kombinationen (Flags)
Manchmal wollen Sie in Enum-Variablen nicht einen einzelnen Zustand, sondern eine Kombination von Zuständen speichern. Ein typisches .NET-Beispiel hierfür ist System.IO. FileAttributes: Damit werden mehrere Attribute von Dateien gleichzeitig ausgedrückt (z.B. Hidden und ReadOnly). Wenn Sie selbst eine derartige Enum-Klasse deklarieren möchten, müssen Sie der Definition voranstellen. Die eckigen Klammern bedeuten, dass es sich beim Inhalt um ein so genanntes Attribut handelt, das der nachfolgenden Deklaration zusätzliche Eigenschaften verleiht. Hier wird als Attribut Flags verwendet. Intern bewirkt das, dass die Enum-Klasse zusätzliche Eigenschaften der Klasse System.FlagsAttribute erhält. (Was Attribute eigentlich sind, wird in Abschnitt 7.7 erklärt.)
136
4 Variablen- und Objektverwaltung
Bei der Definition der Konstanten müssen Sie darauf achten, dass jede Konstante eine Zweierpotenz ist (1, 2, 4, 8, 16, 32 etc.). Damit stellen Sie sicher, dass auch jede Kombination von Konstanten eindeutig ist. Sie können bei der Zuweisung der Konstanten auch hexadezimale Werte verwenden (&H1, &H2, &H4, &H8, &H10, &H20 etc.). Das folgende Beispiel zeigt die Definition der Enum-Klasse myPrivileges, mit der Zugriffsrechte verwaltet werden (z.B. für ein Dateisystem, eine Datenbank etc.). Es ist damit beispielsweise möglich, jemanden den Lesezugriff sowie die Ausführung von Programmen zu erlauben, aber Veränderungen zu verbieten. Enum myPrivileges As Integer ReadAccess = 1 WriteAccess = 2 Execute = 4 Delete = 8 End Enum
Bei der Verwendung von myPrivileges-Variablen können Sie die verschiedenen Zustände mit Or verknüpfen. (Beachten Sie, dass die mathematisch ebenfalls korrekte Verknüpfung der Konstanten durch + bei Option Strict nicht zulässig ist!) Dim priv As myPrivileges priv = myPrivileges.ReadAccess Or myPrivileges.Execute ToString liefert nun eine Zeichenkette, die die Kombination aller Enum-Konstanten ausdrückt. (Ohne das Flags-Attribut bei der Enum-Deklaration würde das nicht funktionieren!) s = priv.ToString
' s = "ReadAccess, Execute"
Wenn Sie testen möchten, ob eine Variable eine bestimmte Konstante enthält, können Sie nicht mehr einfach einen Vergleich mit = durchführen (weil dieser Vergleich natürlich False liefert, wenn die Variable eine Kombination von Konstanten enthält). Stattdessen müssen Sie den Vergleich wie das folgende Beispiel mit And formulieren. If (priv And myPrivileges.Execute) 0 Then ... End If
Beachten Sie, dass die Klammern und der Ausdruck 0 erforderlich sind, falls Sie mit Option Strict arbeiten. (a And b liefert einen Integer-Ausdruck, If erwartet aber einen BooleanAusdruck!)
4.4.3
Interna (System.Enum-Klasse)
Intern sind Enum-Konstrukte von der .NET-Klasse System.Enum abgeleitet. Das bedeutet insbesondere, dass Sie alle Eigenschaften und Methoden von System.Enum auf Enum-Konstanten und -Variablen anwenden können. Im Folgenden finden Sie hierfür einige Beispiele.
HINWEIS
4.4 Enum-Aufzählungen
137
Dieser Abschnitt geht auf einige Interna der Verwaltung von Enum-Konstrukten ein. Dabei wird das Wissen bzw. Verständnis von Grundtechniken der objektorientierten Programmierung vorausgesetzt, das erst im weiteren Verlauf dieses Buchs vermittelt wird. Der Abschnitt richtet sich daher an Leser, die schon etwas Erfahrung mit VB.NET haben.
Namen (Zeichenketten) von Enum-Elementen ermitteln: Wenn Sie bei einem gegebenen Enum-Wert den Namen des entsprechenden Elements wissen möchten, verwenden Sie einfach die Methode ToString: Dim s As String, col As myColors col = myColors.Green s = col.ToString 's = "Green"
Alle Namen einer Enum-Aufzählung ermitteln: Die folgende Schleife verwendet GetNames, um alle in myColors definierten Namen anzuzeigen (also "Red", "Green", "Blue" und "Yellow"). Beachten Sie, dass die Kurzschreibweise Enum.GetNames nicht zulässig ist (obwohl System sonst fast immer weggelassen werden kann), weil Enum ein VB.NET-Schlüsselwort ist. GetNames erwartet als Parameter ein Type-Objekt, das den Datentyp beschreibt. Ein derartiges Objekt wird hier mit col.GetType() erzeugt. Dim all() As String, s As String, col As myColors all = System.Enum.GetNames(col.GetType()) For Each s In all Console.WriteLine(s) Next
Enum-Element aus Zeichenkette erzeugen: Die Methode Parse wertet die angegebene Zeichenkette aus und liefert als Ergebnis ein Enum-Objekt, das der Zeichenkette entspricht. Die Definition von Parse gibt an, dass die Methode ein Objekt des Typs Object zurückgibt. Daher muss CType verwendet werden, um eine Umwandlung in ein myColors-Objekt durchzuführen. (CType wird in Abschnitt 4.6.5 beschrieben.) Die folgende Anweisung entspricht col = myColors.Red. Dim col As myColors col = CType(System.Enum.Parse(col.GetType(), "Red"), myColors)
Per Default unterscheidet Parse zwischen Groß- und Kleinschreibung. Wenn Sie das nicht möchten, müssen Sie als dritten Parameter True übergeben: col = CType(System.Enum.Parse(col.GetType(), "rEd", True), myColors) Parse kommt auch mit Enum-Kombinationen zurecht. In der Zeichenkette müssen die einzelnen Namen durch Kommas getrennt werden. Die folgende Anweisung entspricht priv = myPrivileges.ReadAccess Or myPrivileges.WriteAccess. Dim priv As myPrivileges priv = CType(System.Enum.Parse(priv.GetType(), _ "ReadAccess, WriteAccess"), myPrivileges)
138
4 Variablen- und Objektverwaltung
Testen, ob ein Enum-Wert gültig ist: Wenn Sie wissen möchten, ob ein beliebiger Wert einer Enum-Konstante entspricht, können Sie dies mit IsDefined feststellen. Diese Methode liefert True oder False. If System.Enum.IsDefined(col.GetType(), 17) Then ...
Beachten Sie, dass IsDefined für Enum-Kombinationen (siehe oben) ungeeignet ist! IsDefined(priv, 10) würde eigentlich der Kombination WriteAccess Or Delete entsprechen, liefert aber False!
4.4.4
Syntaxzusammenfassung
Aufzählungen deklarieren und verwenden Enum aufz As Byte/Short/Integer/Long element1 [ = wert1] element2 [ = wert2] ... End Enum
deklariert eine Aufzählung. Den Elementen werden automatisch durchlaufende Zahlen zugewiesen, wenn Sie nicht explizit eigene Werte angeben.
Enum komb_aufz As ... element1 = 1 element2 = 2 element3 = 4 ... End Enum
deklariert eine Aufzählung, deren Elemente kombiniert werden können. Die Elemente müssen dazu Zweierpotenzen enthalten.
Dim aufz_obj As aufz
deklariert aufz_obj als Variable der Aufzählung.
Eigenschaften und Methoden der Klasse System.Enum System.Enum.GetNames(aufz.GetType())
liefert ein Zeichenkettenfeld, das die Namen aller Enum-Konstanten enthält.
System.Enum.IsDefined(aufz.GetType(), n)
testet, ob n ein gültiger Wert einer EnumKonstante von aufz ist.
aufz_obj = CType(System.Enum.Parse( _ col.GetType(), s), aufz)
wertet die Zeichenkette s aus und liefert die entsprechende Enum-Konstante von aufz.
4.5 Felder
4.5
139
Felder
VERWEIS
Felder kommen immer dann zum Einsatz, wenn Sie mehrere gleichartige Daten (z.B. Zeichenketten, Integer-Zahlen etc.) effizient verwalten möchten. Felder sind nicht die einzige Möglichkeit zur Verwaltung von Daten. Die .NET-Bibliothek bietet eine ganze Gruppe so genannter Collection-Klassen, die für Spezialanwendungen effizienter sind als Felder. Damit können Sie beispielsweise assoziative Felder bilden (bei denen ein beliebiges Objekt und nicht eine Integer-Zahl als Index gilt), komfortabel Elemente einfügen und löschen etc. Einen Überblick über die wichtigsten Collection-Klassen sowie Tipps zu ihrer Anwendung gibt Kapitel 9.
4.5.1
Syntax und Anwendung
Felder werden in VB.NET ganz ähnlich wie Variablen deklariert. Der einzige Unterschied besteht darin, dass an den Variablennamen oder an den Variablentyp ein Klammernpaar angehängt werden muss, um so zu kennzeichnen, dass es sich um ein Feld handelt. Dim a() As Integer Dim a As Integer()
'Elementzahl noch unbekannt 'gleichwertige Alternative
Im Regelfall geben Sie auch gleich die Anzahl der Elemente an. In diesem Fall muss die Anzahl im ersten Klammernpaar angegeben werden. Eine Besonderheit von Visual Basic besteht darin, dass Dim b(3) ein Feld mit vier Elementen deklariert: a(0), a(1), a(2) und a(3). Der Zugriff auf einzelne Elemente erfolgt in der Form feldname(indexnummer). Dim b(3) As Integer b(0) = 17 b(1) = 20 b(2) = b(0) + b(1) b(3) = -7
'eindimensionales Feld, vier Elemente
Sie können Felder auch direkt bei der Deklarierung initialisieren. Bei dieser Syntaxvariante dürfen Sie allerdings keine Elementzahl angeben – VB.NET entscheidet selbst, wie viele Elemente erforderlich sind. (Beim folgenden Beispiel hat c drei Elemente, c(0), c(1) und c(2).) Dim c() As Integer = {7, 12, 39}
Wenn Sie ein Feld an eine Methode übergeben, können Sie das Feld auch dynamisch beim Aufruf der Methode übergeben. Die folgende Zeile führt die Methode AddRange für ein ListBox-Steuerelement aus. Diese Methode erwartet ein beliebiges Feld als Parameter. ListBox1.Items.AddRange(New String() {"a", "b", "c"})
140
4 Variablen- und Objektverwaltung
Felder neu dimensionieren Eine Besonderheit von VB.NET besteht darin, dass Sie Felder mit ReDim nachträglich neu dimensionieren können. Wenn Sie dabei das optionale Schlüsselwort Preserve angeben, bleibt der Inhalt des bisherigen Felds erhalten. (Beachten Sie bitte, dass ReDim ein verhältnismäßig zeitaufwendiger Vorgang ist. Wenn Sie eine große Anzahl von Elementen verwalten, wobei die genaue Anzahl von vornherein nicht feststeht, sollten Sie entweder auf eine der in Kapitel 9 beschriebenen Collection-Klassen zurückgreifen oder die Feldanzahl nur in großen Schritten erhöhen.) ReDim a(7) a(5) = 1232 ReDim Preserve a(12)
Mehrdimensionale Felder Mehrdimensionale Felder werden einfach dadurch erzeugt, dass Sie bei Dim mehrere Indizes angeben. Beim folgenden Feld reicht der erste Index von 0 bis 3, der zweite von 0 bis 4, der dritte von 0 bis 5. Insgesamt hat das Feld somit 4*5*6=120 Elemente. Dim d(3, 4, 5) As Integer ReDim kann auch auf mehrdimensionale Felder angewendet werden, allerdings darf dabei nur die Größe der äußersten Dimension verändert werden. (Nach Dim a(3,4,5) dürfen Sie also ReDim a(3,4,6) ausführen, nicht aber ReDim a(4,4,5).)
Feldgröße ermitteln Die Eigenschaft feld.Rank liefert die Anzahl der Dimensionen. (d.Rank liefert 3.) Für jede Dimension kann der zulässige Indexbereich mit feld.GetLowerBound(n) und feld.GetUpperBound(n) ermittelt werden, wobei n die Dimension ist. (Für die erste Dimension gilt n=0!) Die Gesamtzahl aller Elemente kann mit feld.Length ermittelt werden. Die folgenden Zeilen zeigen die Initialisierung eines dreidimensionalen Felds. For i = 0 To d.GetUpperBound(0) For j = 0 To d.GetUpperBound(1) For k = 0 To d.GetUpperBound(2) d(i, j, k) = i * 100 + j * 10 + k Next Next Next
Statt GetUpper/LowerBound können Sie auch die VB-Schlüsselwörter UBound bzw. LBound verwenden. Dabei müssen Sie aber beachten, dass die erste Dimension nun mit n=1 ausgedrückt wird.
4.5 Felder
141
HINWEIS
For i = 0 To UBound(d, 1) For j = 0 To UBound(d, 2) For k = 0 To UBound(d, 3) ...
Bei Feldern, die Sie selbst mit Dim erzeugt haben, können Sie auf die Auswertung von GetLowerBound verzichten: VB.NET-Felder beginnen immer mit dem Index 0. Bei Feldern, die von anderen Bibliotheken stammen, ist eine Auswertung aber sehr wohl sinnvoll, weil .NET grundsätzlich auch Felder mit einem von 0 abweichenden Startindex unterstützt.
For-Each-Schleifen Eine besonders einfache Form, alle Elemente eines Felds zu durchlaufen, bilden die in Abschnitt 5.2.2 vorgestellten For-Each-Schleifen. Die Schleifenvariable muss dabei denselben Datentyp wie das Feld aufweisen. (Durch die Anweisung Console.WriteLine wird der Inhalt von i in einem Konsolenfenster angezeigt.) Dim c() As Integer = {7, 12, 39} Dim i As Integer For Each i In c Console.WriteLine(i) Next
Felder löschen Mit Erase können Sie alle Elemente eines Felds löschen und den so reservierten Speicher wieder freigeben. Erase a, b, c, d
HINWEIS
4.5.2
Interna und Programmiertechniken (System.Array-Klasse)
Dieser Abschnitt beschreibt einige Interna bei der Verwaltung von Feldern. Dabei wird das Wissen bzw. Verständnis von Grundtechniken der objektorientierten Programmierung vorausgesetzt, das erst im weiteren Verlauf dieses Buchs vermittelt wird. Der Abschnitt richtet sich daher an Leser, die schon etwas Erfahrung mit VB.NET haben.
142
4 Variablen- und Objektverwaltung
Intern sind alle Felder von der .NET-Klasse System.Array abgeleitet. Das bedeutet, dass Sie alle Eigenschaften und Methoden von System.Array zur Bearbeitung von Feldern anwenden können. Die im vorigen Abschnitt vorgestellten Eigenschaften bzw. Methoden GetUpper/LowerBound, Length und Rank sind dafür einfache Beispiele. Die folgenden Abschnitte beschreiben einige weitergehende Möglichkeiten. Beachten Sie bitte, dass die Methoden von System.Array zum Teil direkt auf Feldvariablen angewendet werden können (z.B. feld.Rank), zum Teil aber das Feld als Parameter erwarten (z.B. Array.Referse(feld)).
Feldelemente löschen Clear setzt eine vorgegebene Anzahl von Elementen auf 0, Nothing oder False (je nach Datentyp des Elements). Bei einem Integer-Feld entspricht Array.Clear(f, 7, 2) den Anweisungen f(7)=0 und f(8)=0.
Felder kopieren Mit Clone können Sie eine vollständige Kopie eines Felds erzeugen. Clone liefert allerdings ein Object-Feld, das mit CType in ein Integer-Feld umgewandelt werden muss. Die folgenden Zeilen zeigen die Anwendung der Methode. Wenn Sie anschließend b(3) auswerten, enthält dieses Element erwartungsgemäß den Wert 5. (CType wird in Abschnitt 4.6.5 beschrieben.) Dim a(10) As Integer Dim b() As Integer a(3) = 5 b = CType(a.Clone, Integer())
Wenn Sie nicht einfach alle Elemente kopieren möchten, können Sie die Methode Copy zu Hilfe nehmen. Im folgenden Beispiel werden aus dem Feld c sechs Elemente nach d kopiert, wobei das Kopieren in c beim Element 2 und in d beim Element 0 beginnt. Die CopyAnweisung entspricht also d(0)=c(2), d(1)=c(3), d(2)=c(4) etc. Im Konsolenfenster werden daher die Werte 2, 3, 4, 5, 6 und 7 ausgegeben. Dim c(10) As Integer Dim d(5) As Integer Dim i As Integer For i = 0 To 10 c(i) = i Next Array.Copy(c, 2, d, 0, 6) For i = 0 To 5 Console.WriteLine(d(i)) Next
HINWEIS
4.5 Felder
143
Wenn Sie ein Feld kopieren, das Objekte von Referenztypen enthält (keine ValueType-Daten), dann wird ein so genanntes shallow copy durchgeführt. Das bedeutet, dass nur die Referenzen kopiert werden, dass aber von den Objekten selbst keine Kopie erstellt wird. Beide Felder verweisen dann auf dieselben Objekte.
Reihenfolge der Elemente vertauschen Array.Reverse dreht die Reihenfolge der Elemente um. Das letzte Element eines eindimensionalen Felds wird damit zum ersten (und umgekehrt). For i = 0 To 9 e(i) = i Next Array.Reverse(e)
'nun gilt e(0)=9, e(1)=8 etc.
Felder sortieren und durchsuchen Mit Array.Sort können Sie das als Parameter übergebene Feld sortieren. Durch optionale Parameter kann auch nur ein Teil des Felds sortiert werden. Ein Sortieren ist nur möglich, wenn die im Feld gespeicherten Objekte (z.B. Zahlen, Zeichenketten) vergleichbar sind. Wenn Sie ein bestimmtes Element in einem Feld suchen, müssen Sie in der Regel alle Elemente durchlaufen. Wenn das Feld aber bereits sortiert ist, können Sie die Suche ganz wesentlich beschleunigen, indem Sie die Methode BinarySearch zu Hilfe nehmen. Diese Methode benötigt beispielsweise zur Suche in einem Feld mit 1000 Einträgen nur maximal zehn Vergleichsvorgänge. BinarySearch liefert als Ergebnis entweder die positive Indexnummer des gefunden Elements oder eine negative Nummer, wenn das Element nicht gefunden wurde. ' Beispiel variablen\felder Sub sort_array() Dim i As Integer Dim n As String 'einige VB-Autoren ... Dim s() As String = _ {"Holger Schwichtenberg", "Frank Eller", "Dan Appleman", _ "Brian Bischof", "Gary Cornell", "Jonathan Morrison", _ "Andrew Troelsen"} ' sortieren Array.Sort(s) ' suchen i = Array.BinarySearch(s, "Dan Appleman") Console.WriteLine("Suche nach Dan Appleman: Index = " + _ i.ToString + " Element = " + s(i)) End Sub
VERWEIS
144
4 Variablen- und Objektverwaltung
Optional können Sie sowohl an Sort als auch an BinarySearch ein Objekt übergeben, dessen Klasse die Schnittstelle Collections.IComparer realisiert: Dann werden bei jedem Vergleichsvorgang zwei Elemente des Felds an die Compare-Funktion dieser Klasse übergeben. Die Funktion vergleicht die beiden Elemente und liefert als Ergebnis -1, 0 oder 1, je nachdem, ob das erste Objekt kleiner, gleich oder größer als das zweite war. Auf diese Weise können Sie die eingebaute Sort-Methode mit beliebigen Vergleichskriterien verbinden. Beispielsweise könnten Sie das obige String-Feld dann nach Familiennamen sortieren. Hintergrundinformationen und Beispiele zum individuellen Sortieren von Feldern und Aufzählungen finden Sie in Abschnitt 9.3.2.
Asymmetrische Felder Mit Dim können Sie nur symmetrische Felder erzeugen, also beispielspielsweise ein Feld mit 10*10 Elementen. In manchen, überwiegend mathematischen Anwendungen wären aber asymmetrische Felder sinnvoll, bei denen die Anzahl der Elemente in jeder Zeile (in jedem Segment) variiert. Das folgende Beispiel zeigt, wie eine Matrix verwaltet werden kann, bei der in der Zeile n jeweils n Elemente gespeichert werden können (in der ersten Zeile also ein Element, in der zweiten Zeile zwei etc.). Dazu wird arr als ein Feld deklariert, dessen Elemente den Typ Array haben, also selbst wieder Felder enthalten dürfen. Diese Felder werden in einer Schleife mit der Methode CreateInstance erzeugt. An die Methode müssen der gewünschte Datentyp sowie die Anzahl der Elemente übergeben werden. Leider haben mit CreateInstance erzeugte Felder zwei wesentliche Nachteile: Erstens können die Feldelemente nicht mit arr(i,j) angesprochen werden. Stattdessen müssen die Elemente mit arr(i).GetValue(j) gelesen bzw. mit arr(i).SetValue(val, j) verändert werden. Zweitens ist das Feld intern immer ein Object-Feld. Wenn Sie ValueType-Daten (Werttypen) speichern, werden die Elemente intern durch boxing in Objekte umgewandelt. Daher ist die Verwaltung asymmetrischer Felder relativ langsam, womit das hier vorgestellte Verfahren für mathematische Algorithmen leider ungeeignet ist. ' Beispiel variablen\felder Sub asymetric_array() Const n As Integer = 5 Dim i, j As Integer ' Feld deklarieren Dim arr(n - 1) As Array For i = 0 To n - 1 arr(i) = Array.CreateInstance(i.GetType(), i + 1) Next
4.5 Felder
145
' Feld initialisieren For i = 0 To n - 1 For j = 0 To i arr(i).SetValue(i * 100 + j, j) Next Next ' Feldinhalt anzeigen For i = 0 To n - 1 For j = 0 To i Console.Write(arr(i).GetValue(j).ToString + " Next Console.WriteLine() Next End Sub
")
Das Unterprogramm liefert folgendes Ergebnis im Konsolenfenster: 0 100 200 300 400
101 201 301 401
4.5.3
202 302 402
303 403
404
Syntaxzusammenfassung
Felder deklarieren und verwenden Dim f(7) As datentyp
deklariert ein eindimensionales Feld mit acht Elementen, die mit feld(0) bis feld(7) angesprochen werden.
Dim f(n, m, o, p) As datentyp
deklariert ein vierdimensionales Feld.
ReDim [Preserve] f(n)
verändert die Größe des Felds (optional unter Erhaltung des Inhalts).
Erase f
löscht das Feld.
LBound(f,n) UBound(f, n)
ermittelt den minimalen bzw. maximalen Index des Felds für die Dimension n (mit n=0 für die erste Dimension).
146
4 Variablen- und Objektverwaltung
Eigenschaften und Methoden der Klasse System.Array Array.BinarySearch(f, suchobj) Array.BinarySearch(f, suchobj, icompareobj)
durchsucht das Feld f nach dem Eintrag suchobj. Die Methode setzt voraus, dass das Feld sortiert ist, und liefert als Ergebnis die Indexnummer des gefundenen Eintrags. Optional kann eine eigene Vergleichsmethode angegeben werden.
Array.Clear(f, n, m)
setzt m Elemente beginnend mit f(n) auf 0, Nothing oder False (je nach Datentyp des Elements).
f2 = CType(f1.Clone, datentyp())
weist f2 eine Kopie von f1 zu.
Array.Copy(f1, n1, f2, n2, m)
kopiert m Elemente vom Feld f1 in das Feld f2, wobei n1 der Startindex in f1 und n2 der in f2 ist.
f = Array.CreateInstance(type, n [,m [,o]])
erzeugt ein Feld der Größe (n,m,o), wobei in den einzelnen Elementen Objekte des Typs type gespeichert werden können.
f.GetLowerBound(n) f.GetUpperBound(n)
ermittelt den minimalen bzw. maximalen Index des Felds für die Dimension n (mit n=0 für die erste Dimension).
f.GetValue(n [,m [,o]])
liefert das Element f(n, m, o).
f.Length
ermittelt die Gesamtzahl der Elemente des Felds.
f.Rank
gibt die Anzahl der Dimensionen an.
Array.Reverse(f)
vertauscht die Reihenfolge der Elemente des Felds.
f.SetValue(data, n [,m [,o]])
speichert in f(n, m, o) den Wert data.
Array.Sort(f [ ,icompareobj ] )
sortiert f (unter Anwendung der Vergleichsfunktion des ICompare-Objekts).
4.6
Interna der Variablenverwaltung
Dieser Abschnitt geht auf einige Interna der Variablenverwaltung und insbesondere der Speicherverwaltung ein. Dabei wird das Wissen bzw. Verständnis von Grundtechniken der objektorientierten Programmierung vorausgesetzt, das erst im weiteren Verlauf dieses Buchs vermittelt wird. Der Abschnitt richtet sich daher an Leser, die schon einige Erfahrung mit VB.NET haben.
4.6 Interna der Variablenverwaltung
4.6.1
147
Speicherverwaltung
Wie bereits in Abschnitt 4.1.2 beschrieben, ist in VB.NET jede Variable eine Objektvariable. Um aber elementare Datentypen wie Integer oder Double bzw. einfache Datenstrukturen möglichst effizient verwalten zu können, unterscheidet .NET zwischen Wert- und Referenztypen (bzw. zwischen ValueType-Klassen und gewöhnlichen Klassen). Diese Unterscheidung hat einen ganz wesentlichen Einfluss darauf, wie die Daten im Speicher verwaltet werden: •
Gewöhnliche Objekte (für Referenztypen) werden in einem eigenen Speicherbereich, dem so genannten heap, gespeichert. Das ist deswegen sinnvoll, weil derartige Objekte meistens eine variable Größe haben und weil Objekte typischerweise dynamisch während des Programms erzeugt und wieder gelöscht werden. Um die Speicherverwaltung im heap kümmert sich die .NET-Runtime-Bibliothek. Nicht mehr benötigte Objekte werden bei einer so genannten garbage collection (wörtlich: Müllabfuhr) automatisch wieder freigegeben. Einige Feinheiten der garbage collection werden im nächsten Abschnitt beschrieben. Entscheidend für Sie als Programmierer(in) ist an dieser Stelle nur: Sie brauchen sich im Regelfall nicht selbst um die Speicherverwaltung zu kümmern. Bei Prozeduraufrufen werden in Parametern Zeiger (Referenzen) auf die Daten übergeben (nicht die Daten selbst). Ebenso wird bei einer Variablenzuweisung (obj1 = obj2) nur ein neuer Zeiger (eine neue Referenz) auf das Objekt eingerichtet. obj1 und obj2 zeigen nun also auf dasselbe Objekt, dessen Daten sich im heap befinden.
•
VERWEIS
Gewöhnliche Objekte und ValueType-Daten verhalten sich vollkommen unterschiedlich, wenn sie mit ByVal an eine Prozedur übergeben werden! Lesen Sie unbedingt auch den diesbezüglichen Abschnitt 5.3.4!
TIPP
ValueType-Daten (für Werttypen wie Integer) werden dagegen direkt in Datenstrukturen gespeichert (allocated inline a structure). Bei Prozeduraufrufen werden die Daten in den Stack kopiert. Bei einer Variablenzuweisung (obj1 = obj2) werden die Daten kopiert.
Wenn Sie feststellen möchten, ob eine Objektvariable Referenz- oder ValueType-Daten enthält, werten Sie einfach obj.GetType().IsValueType aus!
Boxed value types Aus Effizienzgründen werden ValueType-Objekte also anders behandelt als gewöhnliche Objekte. Es gibt aber Fälle, wo ValueType-Objekte auch intern wie gewöhnliche Objekte dargestellt werden müssen – beispielsweise, wenn ein Integer-Wert in einer als Object deklarierten Variable, einem Object-Feld oder einer Collection gespeichert wird.
148
4 Variablen- und Objektverwaltung
Dazu wird am heap Speicher für eine Zwischenschicht (einen so genannten wrapper) reserviert. Die Daten des ValueType-Objekts werden dorthin kopiert. Dieser Vorgang wird als boxing bezeichnet, die resultierenden Daten boxed value types. Die Rückverwandlung in gewöhnliche ValueType-Daten heißt dementsprechend unboxing.
4.6.2
Garbage collection
Der Begriff garbage collection (wörtlich übersetzt: Müllabfuhr) bezeichnet das Aufräumen des Speichers. Die garbage collection wird automatisch ausgeführt, wobei Sie als Proramierer normalerweise keinen keinen Einfluss haben, wann das passiert. Vielmehr beobachtet die .NET-Bibliothek die Nutzung von Objekten und den Speicherbedarf und entscheidet selbst, wann der Speicher von nicht mehr benötigten Objekten befreit werden muss. Beachten Sie, dass die garbage collection ausschließlich für den heap gilt, auf dem Objekte von Referenztypen gespeichert werden. ValueType-Daten sind von einer garbage collection nicht betroffen.
Dispose-Methode, IDisposable-Schnittstelle Zu den Werbeversprechungen von .NET zählt, dass Sie sich nun nicht mehr um die Speicherverwaltung zu kümmern brauchen. Nicht mehr benötigte Objekte werden automatisch aus dem Speicher entfernt, ohne dass Sie die Objekte explizit löschen müssen. Wie bei vielen Werbeversprechen ist das aber nur die halbe Wahrheit. Die ganze Wahrheit ist, dass die automatische garbage collection nur für solche Objekte zufriedenstellend funktioniert, deren Klassen die Schnittstelle IDisposeable nicht implementieren. In diese Gruppe fallen zwar viele .NET-Klassen, aber bei weitem nicht alle.
VERWEIS
Daneben gibt es zahlreiche Klassen, die die Schnittstelle IDisposable implementieren und daher die Methode Dispose unterstützen. (Was eine Schnittstelle ist, wird erst in Abschnitt 7.5 ausführlich beschrieben. Die Kurzfassung: Eine Schnittstelle definiert eine Reihe von Eigenschaften und Merkmalen, die eine bestimmte Klasse auszeichnen. Die Formulierung eine Klasse x implementiert eine Schnittstelle y bedeutet, dass die Klasse x neben diversen eigenen Eigenschaften und Methoden auch alle Eigenschaften und Methoden von y kennt.) Eine Liste vieler, wenn auch nicht aller .NET-Klassen, die Dispose kennen, finden Sie in der Online-Hilfe bei der Beschreibung der IDisposable-Schnittstelle. Zu den dort genannten Klassen zählen unter anderem viele Klassen zum Umgang mit Dateien (System.IO.*), die meisten Klassen zur Grafikprogrammierung (System.Drawing.*), die meisten Klassen zur Windows-Programmierung (System.Windows.Forms.*) etc. ms-help://MS.VSCC/MS.MSDNVS.1031/cpref/html/frlrfSystemIDisposableClassTopic.htm
Mit der IDisposable-Schnittstelle sind vor allem Klassen ausgestattet, deren Objekte viel Speicherplatz konsumieren (es also wichtig ist, dass dieser Speicher sofort, und nicht irgendwann, freigegeben wird) oder deren Objekte knappe Ressourcen beanspruchen (z.B. Datenbankverbindungen).
4.6 Interna der Variablenverwaltung
149
Im Zusammenhang mit Dispose gibt es drei Überlebensregeln: •
Wenn eine Klasse die Dispose-Methode kennt, dann muss sie ausgeführt werden, wenn ein selbst erzeugtes Objekt dieser Klasse nicht mehr benötigt wird! Dim bm As New Drawing.Bitmap(100, 100) 'Bitmap mit 100*100 Pixel ... Bitmap bearbeiten, anzeigen, speichern etc. bm.Dispose() 'Speicher wieder freigeben
•
Der Code von Prozeduren, die IDisposable-Objekte verwenden, muss so abgesichert werden, dass Dispose auch dann ausgeführt wird, wenn ein Fehler auftritt. (Das gilt insbesondere dann, wenn Sie Klassenbibliotheken programmieren, die beim Auftreten eines Fehlers nicht gleich beendet werden.)
•
Dispose darf nicht für Objekte verwendet werden, die als Parameter einer Ereignispro-
zedur übergeben werden. Der Grund besteht darin, dass das Objekt unter Umständen von der Prozedur, die das Ereignis ausgelöst hat, noch benötigt wird. Es drängt sich nun die Frage auf: Was passiert, wenn auf die Ausführung von Dispose vergessen wird? Eine allgemeingültige Antwort ist leider nicht möglich, weil die Folgen stark von der Natur der Klasse abhängen. Die folgenden Punkte nennen zwei mögliche Konsequenzen: •
Ihr Programm benötigt mehr Speicher als notwendig und wird deswegen ineffizient. Speicherproblem treten vor allem dann auf, wenn Sie eine Menge Objekte, die viel Speicher beanspruchen, in kurzer Zeit erzeugen, verwenden und dann nicht durch Dispose freigeben. Durch eine garbage collection werden zwar auch solche Objekte früher oder später wieder aus dem Speicher entfernt, bei denen Dispose vergessen wurde, aber manchmal ist später eben schon zu spät: Da beansprucht Ihr Programm vielleicht schon einige Hundert Megabyte RAM und zwingt zur langsamen Auslagerung von RAM auf die Festplatte. Immerhin ist es beruhigend zu wissen, dass das Vergessen von Dispose im Regelfall keinen anhaltenden Speicherverlust (memory leak) verursacht, sondern nur die Effizienz der Speicherverwaltung (unter Umständen erheblich) beeinträchtigt.
•
VERWEIS
Ihr Programm beansprucht wertvolle Ressourcen (etwa den Zugriff auf eine Datei oder Datenbank). Während dieser Zeit sind andere Programme (unter Umständen sogar das eigene Programm!) blockiert. Bei vielen System.IO-Klassen zum Zugriff auf Dateien und Verzeichnisse wird statt Dispose üblicherweise die Methode Close ausgeführt – siehe auch Kapitel 10. Eine ausführliche Diskussion zur Verwendung von Dispose für Grafikobjekte finden Sie in Abschnitt 16.1.3. Darüber hinaus wird Dispose auch im Kontext mit einer ganzen Reihe weiterer Klassen beschrieben – werfen Sie einen Blick in das Stichwortverzeichnis beim Eintrag Dispose!
VERWEIS
150
4 Variablen- und Objektverwaltung
Natürlich können Sie auch eigene Klassen mit der IDisposable-Schnittstelle ausstatten; dann müssen Sie die entsprechende Dispose-Methode selbst programmieren. Konkrete Tipps, wie eine derartige Prozedur aussehen könnte, gibt Abschnitt 7.5.2.
Garbage collection manuell auslösen Normalerweise kümmert sich .NET selbstständig darum, eine garbage collection durchzuführen, wenn dies notwendig erscheint. In seltenen Fällen kann es sinnvoll sein, die garbage collection manuell auszuführen – z.B. wenn Sie wissen, dass Ihr Programm in den nächsten Sekunden keine zeitkritischen Operationen durchführen muss. Dazu führen Sie einfach GC.Collect() aus. Beachten Sie, dass die garbage collection in einem eigenen Thread – also quasi parallel zum Hauptprogramm – ausgeführt wird. Die Collect-Methode initiiert diesen Vorgang nur; anschließend wird das Hauptprogramm sofort fortgesetzt. Wenn Sie warten möchten, bis die garbage collection abgeschlossen ist, müssen Sie anschließend noch GC.WaitForPendingFinalizers() ausführen.
Speicherverbrauch ermitteln Mit GC.GetTotalMemory() können Sie den heap-Speicherbedarf ermitteln. An die Methode müssen Sie True oder False übergeben, je nachdem, ob Sie auf das Ende der durch GetTotalMemory ausgelösten garbage collection warten möchten oder nicht. Der so ermittelte Wert hat allerdings wenig mit dem tatsächlich vom Programm beanspruchten Speicher zu tun. Insbesondere gibt es Objekte, bei denen zwar einige Verwaltungsinformationen am heap, weitere Daten aber in anderen Speicherbereichen abgelegt werden (z.B. Bitmaps). Eine Reihe anderer Speicherbedarfparameter können Sie aus den Eigenschaften eines Diagnostics.Process-Objekts entnehmen. Dazu müssen Sie mit GetCurrentProcess ein ProcessObjekt erzeugen. Anschließend können Sie dessen Eigenschaften auslesen. (Diese Eigenschaften geben eine statische Momentaufnahme wieder. Die Speicherwerte des Objekts verändern sich nur, wenn die Methode Refresh ausgeführt wird.) Mit PrivateMemorySize können Sie den Speicherbedarf des Programms ermitteln soweit der Speicher nicht gemeinsam von mehreren Programmen genutzt wird. Beachten Sie, dass PrivateMemorySize nur unter Windows NT/2000/XP ermittelt werden kann. Unter Windows 98/ME kommt es zu einem Fehler. Dim pr As Diagnostics.Process Dim n1, n2 As Integer pr = Process.GetCurrentProcess() n1 = pr.PrivateMemorySize n2 = pr.PeakVirtualMemorySize ... pr.Dispose()
4.6 Interna der Variablenverwaltung
151
HINWEIS
Mit dem in Abbildung 4.2 dargestellten Beispielprogramm variablen\garbage-collection können Sie die Mechanismen der Speicherverwaltung experimentell erforschen. Mit den verschiedenen Buttons können Sie verschiedene speicherintensive Operationen durchführen (die zum Teil im nächsten Abschnitt beschrieben werden). Dabei werden zwar nie durch Dispose Objekte freigegeben, es wird aber einmal pro Sekunde GetTotalMemory ausgeführt. Dadurch wird regelmäßig eine oberflächliche garbage collection ausgeführt. Mit einem eigenen Button können Sie eine tiefgreifende garbage collection auslösen. Eine garbage collection kann unterschiedlich gründlich durchgeführt werden: Durch GetTotalMemory wird offensichtlich nur eine schnelle, aber eben auch etwas schlampige garbage collection ausgelöst. GC.Collect(n) limitiert die garbage collection auf n Stufen. Erst GC.Collect() führt die garbage collection so gründlich wie möglich durch.
Abbildung 4.2: Speicherbedarf ermitteln
Das Verhalten des Programms ist bisweilen rätselhaft: Beispielsweise wird durch das Erzeugen zahlreicher Bitmaps kaum heap-Speicher beansprucht, dafür aber eine Menge sonstiger Speicher. Der Speicher wird nach einer Weile durch eine automatische garbage collection wieder freigegeben. Der heap-Speicher, der durch das Deklarieren eines großen Felds beansprucht wird, wird beinahe sofort automatisch wieder freigegeben. In diesem Fall bleibt aber der durch die Eigenschaft PrivateMemorySize ermittelte Speicherbedarf auf einem relativ hohem Niveau, das auch durch eine explizite garbage collection nicht oder zumindest nur teilweise verkleinert werden kann.
4.6.3
Speicherbedarf
Einzelne Variablen: Die in Abschnitt 4.2 angegebenen Werte für den Platzbedarf elementarer .NET-Datentypen sind mit Vorsicht zu genießen. Sie gelten nur, wenn die Daten in entsprechend deklarierten Variablen gespeichert werden (also ein Integer-Wert in einer Integer-Variable). Wenn dagegen ein Integer-Wert in einer als Object deklarierten Variable gespeichert wird, werden die Daten als boxed value types dargestellt, wodurch sich ein
152
4 Variablen- und Objektverwaltung
zusätzlicher Speicherbedarf von vermutlich acht Byte ergibt. (Dieser Wert ist nicht dokumentiert, geht aber aus Experimenten mit großen Object-Feldern hervor.) Damit nicht genug: Auch wenn zur Speicherung der eigentlichen Daten einer Boolean-, Byte-, Short- und Char-Variablen nur ein bzw. zwei Byte erforderlich sind, werden tatsächlich meist vier Byte reserviert. Der Grund besteht diesmal darin, dass sich dann für den Variablenzugriff so genannte 32-Bit-Aligned-Adressen ergeben, was die Geschwindigkeit der Codeausführung steigert. Das hat zur Folge, dass zur Speicherung eines Byte-Werts je nach Anwendung bis zu zwölf Byte notwendig sind. Felder: Dennoch ist auch die Behauptung, eine Byte-Variable würde nur ein Byte Speicherplatz beanspruchen, richtig. Wenn Sie nämlich ein großes Byte-Feld deklarieren, beansprucht jedes Element dieses Felds tatsächlich nur ein Byte (hier also insgesamt 10 MB). Dazu kommen noch ein paar Byte zur Verwaltung des Felds, aber bei großen Feldern ist der Overhead jetzt wirklich vernachlässigbar. Dim bytearray( 10000000) As Byte
'10 MByte
Object-Felder: Bei der Deklaration eines Object-Felds werden vier Byte pro Element reserviert. (Diese vier Byte dienen nicht zur Speicherung von Daten, sondern zur Speicherung des Verweises auf die Daten!) Das folgende Feld beansprucht daher ca. 40 MB. Dim myarray(10000000) As Object
'40 MByte
Wenn Sie nun in einer Schleife jedes Feldelement mit einem Boolean-Wert belegen (True), werden die entsprechenden Boolean-Objekte erzeugt und in myarray Verweise darauf eingerichtet. For i = 0 To 10000000 myarray(i) = True Next
Wie groß ist nun der gesamte Speicherbedarf? Es sind beachtliche 160 MB! Dieser Betrag ergibt sich aus den bereits erwähnten 40 MB für myarray sowie aus zwölf Byte für jedes einzelne Feldelement. (Da in jedem einzelnen Element eines Object-Felds ein anderer Objekt- oder Datentyp gespeichert werden könnte, muss auch bei jedem einzelnen Element der gesamte objektorientierte Overhead gespeichert werden.) Bei einem Standard-PC, der mit mindestens 256 MByte RAM ausgestattet ist, spielt es keine große Rolle, ob eine Boolean-Variable nun ein oder zwölf Byte beansprucht. Wenn Sie aber mit großen Feldern zu tun haben, spielt die Wahl des richtigen Datentyps eine wichtige Rolle. String-Felder: Laut Dokumentation beansprucht eine String-Variable zehn Byte plus zwei Byte pro gespeichertem Zeichen. Diese Angabe lässt sich aber nur schwer messen. Daher wurden einige Experimente mit großen String-Feldern durchgeführt. Nicht initialisierte Elemente eines String-Felds beanspruchen ebenso wie nicht initialisierte Object-Elemente nur vier Byte. Das folgende Feld belegt daher vorerst nur ca. 40 MByte. Dim s(10000000) As String
'40 MByte
4.6 Interna der Variablenverwaltung
153
Überraschenderweise erhöht auch die folgende Schleife den Speicheraufwand nicht nennenswert. Es wird nämlich nur eine einzige Zeichenkette mit dem Inhalt "x" erzeugt! Alle zehn Millionen String-Variablen verweisen darauf. (Das ist deswegen möglich, weil Zeichenketten als unveränderliche Objekte gelten. Sobald der Inhalt eines beliebigen Elements dieses Felds verändert wird, wird automatisch Platz für die neue Zeichenkette reserviert.) For i = 0 To 10000000 s(i) = "x" Next
'noch immer 40 MByte
Wenn Sie dagegen allen Elementen des Felds zufällige Zeichenketten (jeweils nur ein Zeichen lang) zuweisen, steigt der Speicherverbrauch dramatisch auf ca. 240 MByte an – also auf 24 Byte pro Feldelement. Das ist deutlich mehr, als die VB.NET-Dokumentation vermuten lässt (zehn Byte plus zwei Byte pro Zeichen). Dim r As New Random() For i = 0 To 10000000 s(i) = Chr(r.Next(65, 90)) Next
'ca. 240 MByte 'zufälliger Buchstabe zwischen A und Z
Zum Abschluss noch der Speicheraufwand, wenn etwas längere Zeichenketten verwendet werden: For i = 0 To 10000000 'ca. 280 MByte s(i) = "a" + Chr(r.Next(65, 85)) Next For i = 0 To 10000000 'ca. 320 MByte s(i) = "abcd" + Chr(r.Next(65, 85)) Next
Eine plausible Begründung für den nicht der Dokumentation entsprechenden Speicherverbrauch kann ich leider nicht anbieten. Das einzige Fazit kann eigentlich nur lauten, der Dokumentation nicht einfach blind zu vertrauen, sondern dann und wann ein kleines Experiment durchzuführen.
4.6.4
Variablen- bzw. Objekttyp feststellen (GetType)
Dieser Abschnitt stellt einige Methoden vor, mit denen Sie den Typ einer Variable feststellen können. (Zu diesem Abschnitt gibt es das Beispielprogramm variablen\var-types, das aber aus Platzgründen und wegen des geringen Informationswerts nicht abgedruckt ist. Sie können das Beispielprogramm als Ausgangspunkt für eigene Experimente mit den hier vorgestellten Methoden verwenden.) In diesem Zusammenhang ist bemerkenswert, dass in VB.NET jede Variable bzw. jedes Objekt zur Laufzeit (also bei der Programmausführung) weiß, welchen Datentyp sie enthält bzw. von welcher Klasse es abgeleitet ist. Die hier vorgestellten Funktionen helfen nur beim Ermitteln dieser Informationen.
154
4 Variablen- und Objektverwaltung
TypeName Die Visual-Basic-Methode TypeName liefert eine Zeichenkette mit dem Variablentyp (den Klassennamen) der angegebenen Variable. Für Dim i As Integer liefert TypeName(i) die Zeichenkette Integer. Bei noch nicht initialisierten reference-Objekten liefert TypeName als Ergebnis "Nothing". Bei initialisierten reference-Objekten liefert TypeName den tatsächlichen Datentyp. Wenn Sie also Dim o As Object und dann o=1.5 ausführen, liefert TypeName das Ergebnis "Double". Bei Objekten, deren Klassenname aus mehreren Teilen zusammengesetzt ist, liefert TypeName nur den letzten Teil (also z.B. "FileInfo" bei einem Objekt der Klasse System.IO.FileInfo).
TypeOf obj Is className In manchen Fällen ist der Operator TypeOf eine Alternative zu TypeName. TypeOf muss immer mit Is kombiniert werden. Die Syntax sieht so aus: Dim o As Object o = ... If TypeOf o Is System.IO.FileInfo Then ... End If
Die obige Abfrage gilt dann als erfüllt, wenn o ein Objekt der Klasse System.IO.FileInfo oder einer davon abgeleiteten Klasse enthält. Wenn o noch nicht initialisiert ist (also Nothing enthält), ist die TypeOf-Abfrage nicht erfüllt. Der Vorteil von TypeOf besteht darin, dass der Typenvergleich viel schneller ausgeführt wird als durch If TypeName(o)="System.IO.FileInfo". Allerdings kann TypeOf ausschließlich für Referenztypen eingesetzt werden, nicht aber für Werttypen (ValueType-Klassen). Bei Objekten, deren Klasse aufgrund von Vererbung auf anderen Basisklassen aufbaut, kann TypeOf auch zum Test dieser Basisklassen verwendet werden. Das folgende Beispiel basiert auf einem Objekt der Klasse System.IO.FileInfo, deren Klassenhierarchie hier dargestellt wird. Klassenhierarchie für System.IO.FileInfo Object └─ MarshalByRefObject
│ └─ IO.FileSystemInfo └─ IO.FileInfo
.NET-Basisklasse Objekt nur als Referenz an andere Rechner weitergeben Basisklasse für DirectoryInfo und FileInfo Informationen über Dateien ermitteln
Aufgrund dieser Hierarchie sind alle vier folgenden If-Abfragen erfüllt.
4.6 Interna der Variablenverwaltung
155
Dim o As Object o = New IO.FileInfo("c:\readme.txt") If TypeOf o Is Object Then ... If TypeOf o Is MarshalByRefObject Then ... If TypeOf o Is IO.FileSystemInfo Then ... If TypeOf o Is IO.FileInfo Then ...
VERWEIS
Da jede Klasse von der Basisklasse Object abgeleitet ist, liefert TypeOf o Is Object immer True! Mehr Informationen zu TypeName und TypeOf finden Sie in der Hilfe, wenn Sie nach Bestimmen des Objekttyps suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/vbcn7/html/vbconDiscoveringClassObjectBelongsTo.htm
IsArray, IsDate, IsNumeric, IsReference etc. VB.NET kennt eine Reihe von IsXxx-Methoden, die bei der Klassifizierung von Objekten bzw. Daten helfen. Methoden zur Objektklassifizierung (Klasse Microsoft.VisualBasic.Information) IsArray(var)
liefert True, wenn die Variable ein initialisiertes Feld enthält. (Das Feld muss mit der Elementzahl deklariert sein. Für Dim var() As Short liefert die Methode False.)
IsDate(var)
liefert True, wenn die Variable als Date-Variable deklariert ist oder wenn es sich um eine Zeichenkette handelt, die in der aktuellen Ländereinstellung in ein Datum oder in eine Zeit umgewandelt werden kann.
IsError(var)
liefert True, wenn var ein Objekt einer Klasse ist, die von System.Exception abgeleitet ist. (Derartige Objekte werden in VB.NET zur Darstellung von Fehlern verwendet – siehe Kapitel 11.)
IsNothing(var)
liefert True, wenn var ein nicht initialisiertes reference-Objekt ist. Bei Variablen für Werttypen liefert IsNothing immer False (d.h., auch IsNothing(0) oder IsNothing("") liefert False).
IsNumeric(var)
liefert True, wenn var als Boolean, Byte, Short, Integer, Long, Decimal, Single oder Double deklariert ist, oder wenn var eine Zeichenkette enthält, die als Zahlenwert interpretiert werden kann (z.B. "123").
IsReference(var)
liefert True, wenn die Variable ein initialisiertes reference-Objekt enthält. Wenn var ein Objekt eines Werttyps (ValueType-Objekt) oder Nothing enthält, liefert die Methode False.
156
4 Variablen- und Objektverwaltung
GetType Wenn Ihnen die oben beschriebenen Methoden zu wenig Informationen geben, können Sie mit der Methode GetType ein Objekt der Klasse System.Type ermitteln. Dim t As Type t = variable.GetType()
Das Objekt t gibt nun Auskunft über unzählige Details der zugrunde liegende Klasse: t.name liefert beispielsweise die Kurzform des Klassennamens, t.FullName den vollständigen Klassennamen, t.AssemblyQualifiedName gibt Informationen über die Bibliothek, aus der die Klasse stammt, t.IsValueType gibt an, ob es sich um ein gewöhnliches Objekt oder um einen Werttyp handelt, etc.
VERWEIS
Statt mit var.GetType() können Sie ein Type-Objekt für eine bestimmte Klasse auch durch Type.GetType("klassenname") ermitteln. Type.GetType("Integer") liefert ein Type-Objekt, das die Integer-Klasse beschreibt. Die Type-Klasse kennt Dutzende weitere Eigenschaften und Methoden, mit denen Sie alle nur erdenklichen Informationen über die Klasse ermitteln können. Beispielsweise können Sie damit feststellen, welche Methoden es für die Klasse gibt, welche Ereignisse unterstützt werden etc. Bei der Auswertung dieser Informationen helfen die zahlreichen Klassen des System.Reflection-Namensraums. Beinahe die einzige Information, die Sie leider nicht ermitteln können, ist der Variabelname. Ein Objekt kann keine Informationen darüber, wie die Variable heißt, die auf das Objekt verweist. (Dafür gibt es viele Gründe. Einer besteht ganz einfach darin, dass mehrere Variablen auf ein- und dasselbe Objekt verweisen können.
' Beispiel variablen\var-types Dim fi As New IO.FileInfo("c:\datei1.txt") Dim t As Type = fi.GetType() Console.WriteLine("t.Name: " + t.Name) Console.WriteLine("t.FullName: " + t.FullName) Console.WriteLine("t.AssemblyQualifiedName: " + _ t.AssemblyQualifiedName)
Durch die obigen Anweisungen werden im Konsolenfenster die folgenden Informationen ausgegeben: t.Name: FileInfo t.FullName: System.IO.FileInfo t.AssemblyQualifiedName: System.IO.FileInfo, mscorlib, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
HINWEIS
4.6 Interna der Variablenverwaltung
157
In VB.NET kann GetType auch in der Form GetType(Integer) oder GetType(IO.FileInfo) verwendet werden. GetType liefert damit ein Type-Objekt des angegebenen Typs bzw. der angegebenen Klasse zurück. GetType gilt hier nicht als Methode, sondern als Operator.
4.6.5
Variablen- und Objektkonvertierung (casting)
Der Begriff casting bezeichnet die Umwandlung von Daten zwischen verschiedenen Klassen bzw. Datentypen. In VB.NET ist dafür die Funktion CType vorgesehen, deren Syntax folgendermaßen aussieht: obj2 = CType(obj1, klassenname) CType wird nur dann fehlerfrei ausgeführt, wenn eine Typumwandlung tatsächlich mög-
lich ist. Grundsätzlich gibt es dabei zwei Fälle: •
Wenn der ursprüngliche Objekttyp und die neue Klasse fundamental voneinander abweichen, muss eine Umrechnung bzw. Umwandlung in den neuen Typ durchgeführt werden. Das ist nur dann möglich, wenn die Ausgangsklasse eine entsprechende Umwandlungsmethode vorsieht. Das ist nur bei den elementaren Datentypen der Fall. Deswegen können Sie beispielsweise eine Integer-Zahl in eine Zeichenkette umwandeln. Dim i As Integer = 1000 Dim s As String s = CType(i, String)
•
Sehr oft muss der Typ gar nicht geändert werden – es liegt schon der richtige Typ vor. Allerdings ist die Variable als Object deklariert, weswegen der Compiler nicht weiß, welche Eigenschaften und Methoden zur Verfügung stehen. (Im laufenden Programm weiß das Objekt sehr wohl, welcher Klasse es angehört, aber das nützt dem Compiler nichts.) In solchen Fällen ist obj2 = CType(obj1, klasse) nur eine Hilfe für den Compiler, mit der er den Code überprüfen kann. Durch CType werden die Daten an sich aber nicht verändert, weswegen diese (eigentlich fiktive) Umwandlung auch nicht mehr Zeit kostet als eine gewöhnliche Variablenzuweisung.
Am einfachsten ist dieser zweite Fall anhand eines Beispiels zu verstehen. Die Prozedur write_duplicate hat die Aufgabe, den doppelten Wert des übergebenen Parameters auszugeben. Der Parameter ist als Object deklariert. Innerhalb der Prozedur wird nun mit TypeOf getestet, ob es sich beim Parameter um einen Integer-Wert handelt. Wenn das der Fall ist, wird mit CType eine Typumwandlung durchgeführt. Damit weiß der Compiler nun, dass i eine Integer-Variable ist, und kann i*2 berechnen. Wenn der Parameter eine Zeichenkette enthält, wird diese analog per CType einer String-Variablen zugewiesen. Anschließend kann die Zeichenkette durch s+s verdoppelt werden.
158
4 Variablen- und Objektverwaltung
Sub write_duplicate(ByVal obj As Object) Dim i As Integer Dim s As String If TypeOf obj Is Integer Then i = CType(obj, Integer) Console.WriteLine(i * 2) ElseIf TypeOf obj Is String Then s = CType(obj, String) Console.WriteLine(s + s) Else Console.WriteLine("invalid type") End If End Sub
Da der Parameter von write_duplicate als Object deklariert ist, kann jeder beliebige Wert an die Prozedur übergeben werden. write_duplicate(3) write_duplicate("abc") write_duplicate(0.4)
Als Ausgabe erhalten Sie im Konsolenfenster die folgenden drei Zeilen:
VERWEIS
6 abcabc invalid type
Wenn Sie im Stichwortverzeichnis unter CType nachsehen, finden Sie dort einige Verweise zu Beispielen, die eine reale Anwendung dieser Funktion zeigen. Allerdings sind zum Verständnis der Beispiele oft viele Hintergrundinformationen erforderlich, weswegen hier ein einfaches (aber unrealistisches) Beispiel präsentiert wurde.
CType und die Objekthierarchie Bei Objekten, deren Klassen von anderen Basisklassen abgeleitet sind, kann CType eine Umwandlung in all diese Klassen durchführen. Zur Verdeutlichung dieses Umstands wird nochmals ein Objekt der Klasse IO.FileInfo verwendet. (Die Vererbungshierarchie dieser Klasse ist im vorigen Abschnitt in einem Diagramm dargestellt.) In obj1 wird ein Objekt der Klasse IO.FileInfo gespeichert. Wegen der Klassenhierarchie können die folgenden drei Zuweisungen mit CType problemlos durchgeführt werden. (Anschließend verweisen alle vier Variablen auf dasselbe Objekt!) Dim obj1, obj2 As Object Dim fsi As IO.FileSystemInfo Dim fi As IO.FileInfo
4.6 Interna der Variablenverwaltung
159
obj1 = New IO.FileInfo("c:\readme.txt") fsi = CType(obj1, IO.FileSystemInfo) fi = CType(obj1, IO.FileInfo) obj2 = CType(fi, Object)
Obwohl also alle vier Variablen auf dasselbe Objekt verweisen, akzeptiert der Compiler die Verwendung von Eigenschaften und Methoden zur Bearbeitung des Objekts nur bei den Variablen, die die richtige Klasse aufweisen. Die Methode Exists ist für die Klasse IO.FileSystemInfo deklariert, die Methode DirectoryName für IO.FileInfo. Exists kann daher sowohl auf fsi als auch auf fi angewendet werden, DirectoryName dagegen nur auf fi.
VERWEIS
Console.WriteLine(fsi.Exists()) Console.WriteLine(fi.Exists()) Console.WriteLine(fi.DirectoryName)
Einige Beispiele zu diesem Abschnitt finden Sie im Beispielprogramm variablen\objektkonvertierung, das hier aber aus Platzgründen nicht abgedruckt ist. Weitere Informationen zur automatischen und expliziten Konvertierung zwischen verschiedenen Daten- und Objekttypen gibt Abschnitt 8.4. Der Schwerpunkt dieses Abschnitts liegt bei der Umwandlung zwischen Basisdatentypen (Zahlen, Zeichenketten, Datum und Uhrzeit). Dort lernen Sie die VB.NET-Funktionen CBool, CByte, CChar, CDate sowie eine ganze Menge weiterer .NET-Methoden kennen.
5
Prozedurale Programmierung
Dieses Kapitel liefert eine kompakte Beschreibung der prozeduralen Sprachelemente von VB.NET. Dazu zählen insbesondere die Kommandos zur Bildung von Schleifen, Abfragen und Prozeduren. Das Kapitel wird mit einer Referenz aller Operatoren abgeschlossen.
VERWEIS
5.1 5.2 5.3 5.4
Verzweigungen (Abfragen) Schleifen Prozeduren und Funktionen Operatoren
162 165 167 181
Objektorientierte Sprachelemente werden in den Kapiteln 6 und 7 behandelt. Eine zumeist gut verständliche Sprachdefinition finden Sie auch in der Online-Hilfe, wenn Sie nach Visual Basic Programmiersprachenspezifikation suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/vbcn7/html/vaconprogrammingwithvb.htm
162
5 Prozedurale Programmierung
5.1
Verzweigungen (Abfragen)
5.1.1
If-Then-Else
Die folgenden Zeilen zeigen anhand eines Beispiels die Struktur von If-Then-Else-Konstruktionen. Dim a As Double = Rnd() If a < 0.1 Then MsgBox("a ist kleiner 0.1") ... beliebig viele weitere Kommandos ElseIf a < 0.5 Then MsgBox("a ist größer gleich 0.1, aber kleiner 0.5") ElseIf a < 0.9 Then MsgBox("a ist größer gleich 0.5, aber kleiner 0.9") Else MsgBox("a ist größer gleich 0.9") End If
In der einfachsten Form besteht die If-Konstruktion nur aus der Abfrage, dem Block nach Then und schließlich End If. Die ElseIf-Blöcke und der Else-Block sind optional. In einfachen Fällen kann eine If-Abfrage in nur einer einzigen Zeile formuliert werden. Dazu wird unmittelbar nach Then das auszuführende Kommando angegeben. Es ist kein End If erforderlich (und bei dieser Syntaxvariante auch gar nicht erlaubt). If a < 0.1 Then MsgBox("a ist kleiner 0.1")
Formulierung von Bedingungen If erwartet als Bedingung einen Wahrheitswert, also True oder False. Wenn Sie einen Vergleich angeben (z.B. a=3 oder b>4), dann liefert der Vergleich diesen Wahrheitswert. Natürlich können Sie auch mehrere Teilbedingungen mit logischen Operatoren miteinander verbinden: If (a < 0.1 And b > 3) Or c=5 Then ...
Wenn Sie dagegen einfach eine Variable angeben, dann testet VB.NET, ob dessen Wert 0 (False) bzw. ungleich 0 (True) ist. Der folgende Then-Block wird also immer ausgeführt, wenn a ungleich 0 ist. If a Then ...
5.1.2
Select-Case
Alternativ zur If-Verzweigung kennt VB.NET auch die Select-Case-Konstruktion, mit der sich manche Abfragen übersichtlicher (lesbarer) formulieren lassen. Abermals vorweg ein Beispiel:
5.1 Verzweigungen (Abfragen)
163
Dim n As Integer = CInt(Rnd() * 100) Select Case n Case 1 MsgBox("n ist 1") Case 2, 3, 4, 5 MsgBox("n liegt zwischen 2 und 5") Case 6 To 50 MsgBox("n liegt zwischen 6 und 50") Case Is < 70 MsgBox("n liegt zwischen 51 und 69") Case Else MsgBox("n ist größer gleich 70") End Select
Mit Select Case wird der auszuwertende Ausdruck angegeben. Dabei muss es sich um einen der elementaren Datentypen handeln (alle Zahlentypen sowie Char, String, Date oder Object). In jeder Case-Bedingung kann ein bestimmter Wert, eine Aufzählung von Werten oder ein Wertbereich (a To b) angegeben werden. Des weiteren kann mit Is ein beliebiger Vergleichsoperator angegeben werden. (Is TypeOf datentyp ist allerdings nicht erlaubt.) Wenn in Select Case eine Zeichenkette angegeben wird, erfolgt der Zeichenkettenvergleich gemäß der Option-Compare-Einstellung (siehe Abschnitt 8.2.3). Per Default bedeutet das, dass zwischen Groß- und Kleinschreibung differenziert wird. Sobald eine zutreffende Case-Bedingung gefunden wird, wird der darauf folgende Codeblock ausgeführt. Anschließend wird die gesamte Konstruktion verlassen. (Es wird also maximal ein Case-Codeblock ausgeführt.)
5.1.3
IIf, Choose, Switch
Die Methode IIf(a, b, c) aus der Interaction-Klasse des Microsoft.VisualBasic-Namensraum entspricht in etwa dem C-Konstrukt a ? b : c: Wenn a zutrifft, liefert die Funktion den Wert von b, sonst c. Dim x As Double = Rnd() Dim s As String s = IIf(x > 0.5, "abc", "def") Choose weist eine ähnliche Syntax wie If auf: Im ersten Parameter wird ein Indexwert angegeben, der zeigt, welchen der folgenden Parameter Choose zurückgeben soll. Wenn der Indexwert kleiner als 1 oder größer als der größtmögliche Index ist, liefert die Funktion als Ergebnis Nothing. Bei Fließkommazahlen werden die Nachkommastellen ignoriert. Choose(1, "a", "b", "c") Choose(2.8, "a", "b", "c") Choose(0, "a", "b", "c") Choose(4, "a", "b", "c")
'liefert 'liefert 'liefert 'liefert
"a" "b" Nothing Nothing
164
5 Prozedurale Programmierung
Eine dritte Variante für die Formulierung einzeiliger Auswahlentscheidungen stellt die Methode Switch dar. In Switch(a,x,b,y,..) wird getestet, ob a ungleich 0 (also True) ist. Wenn ja, liefert Switch als Ergebnis x, andernfalls wird der Test für b wiederholt etc. Ist keine der in jedem zweiten Parameter formulierten Bedingungen wahr, liefert die Funktion als Ergebnis Nothing.
HINWEIS
Switch(1, "a", 1, "b") Switch(0, "a", 1, "b") Switch(0, "a", 0, "b")
'liefert "a" 'liefert "b" 'liefert Nothing
Unabhängig vom Ergebnis der Bedingung werden bei IIf, Choose und Switch alle Parameter ausgewertet. Daher werden bei IIf(True, funktion1, funktion2) sowohl funktion1 als auch funktion2 berechnet, obwohl IIf eigentlich nur den Wert von funktion1 benötigen würde. Dieses Verhalten ist weder besonders logisch noch effizient, stellt aber die Kompatibilität mit VB6 sicher.
5.1.4
Syntaxzusammenfassung
Verzweigungen mit If-Then-Else If bedingung Then kommando
einzeilige Kurzform
If bedingung Then kom1 Else kom2
einzeilige Kurzform mit Else
If bedingung Then ... ElseIf bedingung Then ... Else ... End If
mehrzeilige Variante beliebig viele Kommandos optional, beliebig oft optional mehrzeilige Variante beliebig viele Kommandos
Verzweigungen mit Select-Case Select Case ausdruck Case bedingung1 ... Case bedingung2 ... Case Else ... End Select
beliebig viele Fälle
optional
5.2 Schleifen
165
Case-Bedingungen wert wert1, wert2, wert3 wert1 To wert2 Is operator vergleichswert
Einzelwert (oder Zeichenkette) Aufzählung Wertebereich allgemeiner Vergleich, z.B. Is < 3
Fallunterscheidungen mit IIf, Choose und Switch (Namensraum Microsoft.VisualBasic) IIf(bedingung, ausdruck1, ausdruck2)
die Bedingung entscheidet, welchen Ausdruck IIf als Ergebnis liefert
Choose(index, ausd1, ausd2, ausd3 ...)
der erste Ausdruck wirkt wie ein Index
Switch(bed1, ausd1, bed2, ausd2 ...)
die erste wahre Bedingung entscheidet über den Ergebnisausdruck
5.2
Schleifen
5.2.1
For-Next-Schleifen
Die einfachste Schleifenform ist die klassische For-Next-Schleife. Die folgenden Zeilen zeigen die prinzipielle Syntax. Normalerweise wird die Schleifenvariable mit jedem Durchlauf um 1 erhöht. Sie können aber mit Step einen beliebigen anderen Wert angeben (auch einen negativen!). Bei Next können Sie optional die Schleifenvariable angeben. Das ist zwar nicht notwendig, fördert bei verschachtelten Konstruktionen aber die Lesbarkeit. Dim i As Integer For i = 1 To 10 [ Step 2 ] ... Next [ i ]
Intern wird am Beginn der Schleife getestet, ob die Schleifenvariable bereits größer als der Endwert ist (bzw. kleiner bei einem negativen Step-Wert). In diesem Fall wird die Schleife sofort abgebrochen. Der Schleifenkörper wird dann kein einziges Mal durchlaufen. Jedes Mal, wenn die Programmausführung Next erreicht, wird die Schleifenvariable um 1 bzw. um den Step-Wert erhöht. Anschließend wird der Test wiederholt, ob der Zielwert bereits über- bzw. unterschritten wurde. Wenn das nicht der Fall ist, wird die Schleife nochmal durchlaufen. (Eine Konsequenz dieser Vorgehensweise besteht darin, dass die Schleifenvariable am Ende der obigen Schleife den Wert 11 enthält!) Die Schleife kann vorzeitig mit Exit For abgebrochen werden. (Exit For bewirkt, dass der Code nach dem dazugehörenden Next-Kommando fortgesetzt wird.)
166
5 Prozedurale Programmierung
5.2.2
For-Each-Schleifen
Bei For-Each-Schleifen durchläuft die Schleifenvariable alle Elemente des angegebenen Felds bzw. der Aufzählung. (Der Begriff Aufzählung meint genau genommen ein Objekt, dessen Klasse die IEnumerable-Schnittstelle implementiert. Was das bedeutet, wird in Kapitel 9 ausführlich beschrieben.) Die folgende Schleife verdeutlicht die Syntax. Der Datentyp der Schleifenvariable muss mit dem des Felds bzw. der Aufzählung übereinstimmen. Die Schleife kann vorzeitig mit Exit For abgebrochen werden. Dim i As Integer Dim arr() As Integer = {12, 7, 4} For Each i In arr Console.WriteLine(i) Next
TIPP
For-Each-Schleifen sind im Regelfall nicht dazu geeignet, die Elemente einer Auf-
zählung zu löschen (weil dadurch die Aufzählung durcheinander kommt). Stattdessen sollten Sie eine For-Next Schleife verwenden, die von coll.Count-1 bis 0 herunterzählt und coll(i) löscht.
5.2.3
Do-Loop-Schleifen
Es gibt insgesamt fünf Möglichkeiten, Do-Loop-Schleifen zu bilden, von denen hier zwei dargestellt sind. Die erste Schleife gibt die Werte 1 bis 5 aus, die zweite die Werte 1 bis 4. Dim i As Integer = 1 Do While i < 5 Console.WriteLine(i) i += 1 Loop
Der wesentliche Unterschied zwischen der Formulierung der Schleifenbedingung bei Do oder bei Loop besteht darin, dass der Schleifenkörper im zweiten Fall immer einmal durchlaufen wird. Im ersten Fall kann es dagegen sein, dass die Bedingung von Anfang an nicht erfüllt ist und das Innere der Schleife daher nie durchlaufen wird. Dim j As Integer = 1 Do Console.WriteLine(j) j += 1 Loop Until j > 5
Die anderen Schleifenvarianten bestehen darin, die Do-Bedingung mit Until bzw. die LoopBedingung mit While zu bilden oder eine Schleife ganz ohne Bedingung zu bilden. Die da-
5.3 Prozeduren und Funktionen
167
raus resultierende Endlosschleife kann mit Exit Do abgebrochen werden. (Es ist übrigens nicht zulässig, sowohl bei Do als auch bei Loop eine Bedingung anzugeben.)
5.2.4
Syntaxzusammenfassung
Schleifen For var = start To ende [Step schrittweite] ... [Exit For] Next [var]
durchläuft die Schleife, bis var größer als ende ist (bzw. kleiner, wenn mit Step eine negative Schrittweite angegeben wird).
For Each var In feld ... [Exit For] Next [var]
durchläuft die Schleife, bis var alle Elemente des Felds oder der Aufzählung angenommen hat.
Do [While bedingung oder Until bedingung] ... [Exit Do] Loop
durchläuft die Schleife, bis die WhileBedingung erfüllt bzw. die UntilBedingung nicht mehr erfüllt ist.
oder Do ... [Exit Do] Loop [While bedingung oder Until bedingung]
5.3
Prozeduren und Funktionen
5.3.1
Syntax
Prozedurale Programmiersprachen (zu denen auch VB.NET zählt) zeichnen sich dadurch aus, dass der Programmcode in kleinen, voneinander getrennten Programmteilen angeordnet wird. Diese Programmteile (Prozeduren) können sich gegenseitig aufrufen und dabei Parameter übergeben. Es gibt zwei Typen von Prozeduren: •
Unterprogramme sind Prozeduren, die kein Ergebnis zurückgeben.
•
Funktionen sind Prozeduren, die als Ergebnis einen Wert zurückgeben. Dabei kann der Rückgabewert einen beliebigen Datentyp haben. (Funktionen können also auch Objekte zurückgeben.) VB.NET-Funktionen bieten keine direkte Möglichkeit, mehrere Werte zurückzugeben. Wenn Sie das wollen, müssen Sie entweder ein Objekt oder eine Struktur mit mehreren Eigenschaften zurückgeben oder die Parameter der Funktion mit ByRef deklarieren und im Code verändern. (Details zur Verwendung von ByRef folgen etwas weiter unten.)
VERWEIS
168
5 Prozedurale Programmierung
Im Kontext einer Klasse werden Prozeduren zu Methoden. (Eine Methode ist also nichts anderes als eine Prozedur, die in einer Klasse formuliert wird.)
Bei Konsolenanwendungen (bzw. bei VB.NET-Programmen, die aus nur einem Modul bestehen) hat die Prozedur mit dem Namen Main eine besondere Bedeutung: Die Programmausführung beginnt mit dieser Prozedur. Wenn Main zu Ende ist, endet auch das Programm. (Bei Windows-Anwendungen beginnt die Programmausführung dagegen per Default damit, dass ein Objekt eines Fensters erzeugt und ausgeführt wird. Hier endet die Programmausführung mit dem Schließen des Fensters. Aber auch bei Windows-Anwendungen können Sie die Projekteigenschaften so einstellen, dass das Programm mit Main beginnt.)
Deklaration von Prozeduren Prozeduren werden mit Sub name bzw. mit Function name eingeleitet. Sie enden mit End Sub bzw. mit End Function. Bei der Deklaration von Funktionen muss der Datentyp des Rückgabewerts angegeben werden. Das erfolgt wie bei Variablen durch den Nachsatz As datentyp. Die folgende Funktion liefert als Ergebnis also einen Integer-Wert. Function myFunc() As Integer ... End Function
Wie bei Variablen ist bei der Deklaration auch eine Kurzschreibweise mit Zeichen wie % (Integer) oder $ (String) zulässig: Function myFunc%()
Prozeduren können mit einer (in Klammern deklarierten) Parameterliste ausgestattet werden. Parameter werden wie Variablen deklariert, also jeweils mit der Angabe des Datentyps. Die folgende Funktion erwartet als Parameter einen Integer-Wert. (Details zum Umgang mit Parametern folgen etwas weiter unten in einem eigenen Abschnitt.) Function myFunc(ByVal y As Integer) As Integer
Prozeduren werden im Regelfall bis zu ihrem Ende ausgeführt. Sie können aber auch vorzeitig beendet werden: Unterprogramme mit Exit Sub oder Return, Funktionen mit Exit Function oder Return. Bei Funktionen gibt es zwei Möglichkeiten, den Rückgabewert anzugeben: Üblicherweise wird das Ergebnis als Parameter an Return übergeben. Die andere Variante besteht darin, eine Zuweisung an den Funktionsnamen durchzuführen. (Wenn die Funktion myFunc heißt, können Sie den Rückgabewert beispielsweise durch myFunc = 7 einstellen.) Wenn die Funktion durch Exit Function oder End Function beendet wird, ohne dass vorher ein Rückgabewert eingestellt wurde, dann wird ein nicht initialisiertes Objekt des jeweiligen Datentyps übergeben (0 bei Zahlen, "" bei Zeichenketten, Nothing bei Objekten etc.)
5.3 Prozeduren und Funktionen
169
Es ist nicht möglich, innerhalb einer Prozedur eine andere Prozedur zu deklarieren. (Die Deklaration von Prozeduren darf also nicht verschachtelt werden.)
Aufruf von Prozeduren Prozeduren werden einfach durch die Nennung ihres Namens aufgerufen. Die Entwicklungsumgebung fügt an den Namen automatisch ein Klammernpaar an. Falls an die Prozedur Parameter übergeben werden, müssen diese innerhalb dieser Klammern angegeben werden. Bei Funktionen wird der Rückgabewert üblicherweise ausgewertet (z.B. in einer Bedingung oder in einer Zuweisung). Das ist aber nicht erforderlich, falls Sie aus irgendeinem Grund nicht am Rückgabewert interessiert sind. Daher sind die zwei folgenden Zeilen zum Aufruf von myFunc beide zulässig. myFunc(3) If myFunc(7) > 12 Then ...
Bei Unterprogrammen darf der Aufruf auch mit Call erfolgen. (Call ist ein Relikt von VB6.) Die beiden folgenden Aufrufe sind daher gleichwertig. myProc() Call myProc()
Ein erstes Beispiel Die folgenden Zeilen zeigen die Definition der beiden Prozeduren myProc und myFunc sowie deren Aufruf durch die Prozedur Main. Das Programm ist eine Konsolenanwendung, d.h., der gesamte Code ist von Module Module1 und End Module eingeschlossen. (Diese zwei Zeilen werden in diesem Buch normalerweise nicht abgedruckt. Eine Erklärung, was ein Modul eigentlich ist, folgt in Kapitel 7. Vorerst reicht es aus, wenn Sie wissen, dass Code von Konsolenanwendungen innerhalb von Module-Anweisungen angegeben werden muss. Die Entwicklungsumgebung sieht diese Anweisungen ohnedies automatisch vor.) Das Ergebnis des Programms wird im Konsolenfenster sichtbar: Dort werden die Zeichenkette abc und die Zahl 24 angezeigt, bis Sie durch Return das Programm beenden. ' Beispiel prozedurale-programmierung\prozeduren Module Module1 Sub Main() Dim x As Integer myProc() x = myFunc(7) Console.WriteLine(x) Console.ReadLine() 'auf Return warten End Sub
170
5 Prozedurale Programmierung
Sub myProc() Console.WriteLine("abc") End Sub Function myFunc(ByVal y As Integer) As Integer Return (y + 1) * 3 End Function End Module
Zugriff auf Variablen Innerhalb einer Prozedur kann auf alle Variablen zugegriffen werden, die auf Modul- oder Klassenebene deklariert werden. Es ist aber nicht möglich, auf Variablen zuzugreifen, die in einer anderen Prozedur deklariert sind. Module Module1 Dim var1 As Integer Sub Main() Dim var2 As Integer myProc1() End Sub
VERWEIS
Sub myProc1() var1 = 7 'ok var2 = 3 'Syntaxfehler, auf var2 kann nicht zugegriffen werden End Sub End Module
Die Frage, wo im Code auf welche Variable, Prozedur, Methode etc. zugegriffen werden kann, ist weitaus komplexer, als es hier den Anschein hat. In Abschnitt 7.9 – nach der Präsentation der objektorientierten Sprachelemente von VB.NET – wird das Thema noch einmal aufgegriffen und dann umfassender behandelt. An dieser Stelle wird vorausgesetzt, dass das Programm aus nur einem einzigen Modul oder aus nur einer einzigen Klasse besteht, wie dies bei einfachen Konsolenanwendungen oder bei Windows-Anwendung mit nur einem Fenster der Fall ist.
5.3.2
Lokale und statische Variablen
Innerhalb von Prozeduren können ebenfalls Variablen deklariert werden. Diese Variablen werden oft als lokal bezeichnet, weil sie getrennt von Variablen außerhalb der Prozedur verwaltet werden und nur innerhalb der Prozedur verwendet werden können. (Parameter von Prozeduren werden übrigens wie lokale Variablen behandelt.)
5.3 Prozeduren und Funktionen
171
Es ist zulässig, je eine Variable innerhalb und außerhalb einer Prozedur mit demselben Namen zu deklarieren. Lokale Variablen werden bei jedem Aufruf der Prozedur neu initialisiert (d.h., sie merken sich ihre bisherigen Zustände nicht). Wenn Sie das folgende Miniprogramm ausführen, wird zweimal der Wert 0 und dann einmal der Wert 5 ausgegeben. (i innerhalb von Main wird also durch den Code in myProcVar nicht verändert.) Sub Main() Dim i = 5 myProcVar() myProcVar() Console.WriteLine(i) End Sub Sub myProcVar() Dim i As Integer Console.WriteLine(i) i = 2 End Sub
Statische Variablen Mit Static können Variablen innerhalb einer Prozedur als statisch definiert werden. Das bedeutet, dass sie ihren Wert nach der Rückkehr der Prozedur behalten und beim nächsten Aufruf noch 'wissen', welchen Wert sie zuletzt hatten. Wenn bei der Deklaration eine Zuweisung angegeben wird (wie im folgenden Beispiel), dann wird diese Zuweisung nur beim ersten Aufruf durchgeführt. Wenn Sie das folgende Miniprogramm ausführen, werden die Werte 1, 2 und 3 ausgegeben. Sub Main() myProcStatic() myProcStatic() myProcStatic() End Sub
VORSICHT
Sub myProcStatic() Static i As Integer = 1 Console.WriteLine(i) i += 1 End Sub
Das Schlüsselwort static der Programmiersprache C# hat eine vollkommen andere Bedeutung als das hier beschriebene VB.NET-Schlüsselwort Static! static von C# wird zur Beschreibung gemeinsamer Klassenvariablen bzw. zur Beschreibung von Methoden verwendet, die ohne ein Objekt verwendet werden können. static hat damit dieselbe Bedeutung wie das VB.NET-Schlüsselwort Shared.
172
5.3.3
5 Prozedurale Programmierung
Rekursion
Prozeduren können sich selbstverständlich gegenseitig aufrufen. Das bedeutet, dass Sie z.B. innerhalb von myProc die Funktion myFunc nutzen können etc. Ein Sonderfall derartiger Aufrufe besteht dann, wenn eine Prozedur sich selbst aufruft. Das ist nicht nur wie im folgenden Beispiel zur Berechnung mancher mathematischer Funktionen nützlich, sondern generell zur Verarbeitung hierarchischer Datenstrukturen. Wenn Sie beispielsweise alle Verzeichnisse des Dateisystems durchlaufen möchten, verwenden Sie im Regelfall eine rekursive Prozedur, die sich selbst für alle Unterverzeichnisse aufruft. (Werfen Sie einen Blick in das Stichwortverzeichnis! Dort finden Sie unter dem Stichwort Rekursion eine ganze Reihe von Querverweisen auf Rekursionsbeispiele.) Die folgende Beispielfunktion berechnet die Fakultät einer Zahl. Diese mathematische Funktion ist definiert als das Produkt aller Zahlen zwischen 1 und der angegebenen Zahl. Die Fakultät von 5 beträgt also 1*2*3*4*5=120. Der Aufruf myFuncRec(5) bewirkt, dass in der If-Abfrage 5 * myFuncRec(4) aufgerufen wird, dann 4 * myFuncRec(3), dann 3 * myFuncRec(2) und dann 2 * myFuncRec(1). Durch myFuncRec(1) wird einfach der Long-Wert zurückgegeben. Anschließend lösen sich die Aufrufe rückwärts wieder auf. (Wenn Sie mit dem Konzept der Rekursion nicht vertraut sind, können Sie den Code mit DEBUGGEN|EINZELSCHRITT auch Zeile für Zeile ausführen.) ' Beispiel prozedurale-programmierung\prozeduren Function myFuncRec(ByVal n As Long) As Long If n c And d entspricht also If ((a+b) > c) And d. Eine vollständige Tabelle mit der Hierarchie aller Operatoren finden Sie, wenn Sie in der Hilfe nach Operatorvorrang Visual Basic suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/vblr7/html/vagrpoperatorprecedence.htm
Syntaxzusammenfassung Zuweisungen =
weist einer Variablen oder einem Objekt einen Wert zu (z.B. a=3).
:=
weist einem benannten Parameter einen Wert zu.
+= -=
vergrößert bzw. verkleinert eine Variable um den angegebenen Wert (z.B. a+=1)
*= /=
multipliziert bzw. dividiert eine Variable um den angegebenen Wert.
\=
speichert in der Variable den Rest einer ganzzahligen Division. (a\=3 entspricht a = a Mod 3).
+= &=
fügt einer Zeichenkette eine andere Zeichenkette hinzu.
Verknüpfung von Zeichenketten +
verknüpft Zeichenketten.
&
verknüpft beliebige Datentypen, sofern sie in Zeichenketten umgewandelt werden können.
5.4 Operatoren
183
Arithmetische Operatoren -
ist das negatives Vorzeichen.
+ - * /
führt die Grundrechnungsarten durch.
^
potenziert Zahlen. (2^3 liefert 8.)
\
führt eine ganzzahlige Division für ganze Zahlen durch. (8 \ 3 liefert 2.)
Mod
liefert den Rest einer Division mit einem ganzzahligen Ergebnis. (8.3 Mod 3.1 liefert 2.1. Dieses Ergebnis ergibt sich aus 8.3/3.1=2.67. Der Wert 2.67 wird abgerundet zu 2. Nun wird 8.3 - 2 * 3.1 ermittelt: Ergebnis: 2.1.)
Vergleichsoperatoren =
testet auf Gleichheit.
testet auf Ungleichheit.
< >=
testet, ob der erste Wert größer als der zweite ist (bzw. größer-gleich).
Is
testet, ob zwei Objekte gleich sind.
Like
führt einen Mustervergleich für Zeichenketten durch. ("abcd" LIKE "?b*" liefert True.)
Logische Operatoren And
verknüpft zwei (Wahrheits-)Werte durch logisches Und.
AndAlso
funktioniert wie And, allerdings wird b bei a AndAlso b nur dann ausgewertet, wenn a=True gilt. AndAlso kann nur für Wahrheitswerte verwendet werden.
Or
verknüpft zwei (Wahrheits-)Werte durch logisches Oder.
OrElse
funktioniert wie Or, allerdings wird b bei a OrElse b nur dann ausgewertet, wenn a=False gilt. OrElse kann nur für Wahrheitswerte verwendet werden.
Xor
verknüpft zwei (Wahrheits-)Werte durch logisches Exklusiv-Oder.
Sonstige Operatoren AddressOf fn
ermittelt die Adresse einer Funktion, Prozedur oder Methode.
GetType(c)
liefert ein Type-Objekt für die angegebene Klasse (z.B. GetType(String)).
TypeOf x
testet, ob x ein Objekt einer bestimmten Klasse ist. TypeOf darf nur im Rahmen der Konstruktion If TypeOf x Is abc Then ... eingesetzt werden. x muss ein Objekt eines Referenztyps enthalten, kein ValueType-Objekt!
184
5 Prozedurale Programmierung
Andere Sonderzeichen im Programmcode ' Kommentar
leitet einen Kommentar ein.
#Region
leitet eine Direktive für den Editor oder Compiler ein.
_
gibt an, dass die Programmzeile fortgesetzt wird. (Vor _ muss ein Leerzeichen stehen!)
"abc"
gibt eine Zeichenkette an (String-Datentyp).
"a"C
gibt ein einzelnes Zeichen an (Char-Datentyp).
#12/31/2002#
gibt ein Datum an (optional mit Zeitangabe).
&H123
gibt eine hexadezimale Zahl an.
&O123
gibt eine oktale Zahl an.
123@ oder 123D
gibt eine Decimal-Zahl an.
123! oder 123F
gibt eine Single-Zahl an.
123# oder 123R
gibt eine Double-Zahl an.
123& oder 123L
gibt eine Long-Zahl an.
123% oder 123I
gibt eine Integer-Zahl an.
123S
gibt eine Short-Zahl an.
( ... )
ändert die Abarbeitungsreihenfolge, z.B. (a Or b) And c oder (a+1) * 3).
x(n)
greift auf das n-te Element des Felds x() zu.
{1, 2, 3}
zählt Elemente auf (z.B. Dim a() As Integer = {1, 2, 3}).
[name]
gibt einen Variablen- oder Prozedurnamen an, der denselben Namen wie ein VB.NET-Schlüsselwort hat.
gibt ein Attribut an.
6
Klassenbibliotheken und Objekte anwenden
Ob Sie Ihre eigenen Projekte mit VB.NET objektorientiert konzipieren oder eher traditionell aus Prozeduren und Funktionen aufbauen, bleibt Ihnen überlassen. Auf jeden Fall müssen Sie lernen, mit den durch VB.NET vorgegebenen Klassenbibliotheken zu arbeiten. Ob Sie nun ein einfaches Formular mit Steuerelementen erstellen, mit System.IO-Objekten eine Textdatei erzeugen oder Methoden zur Bearbeitung von Zeichenketten (also String-Objekten!) anwenden – immer haben Sie es mit dem objektorientierten Ansatz von .NET zu tun. Ziel dieses Kapitels ist es, Ihnen die Welt der Objekte nahezubringen. Damit meine ich nicht nur die Erklärung der Nomenklatur (Klasse, Objekt, Methode, Eigenschaft, Ereignis etc.), sondern auch den Umgang mit dem Objektbrowser sowie die Kunst, die umfassende Online-Dokumentation richtig zu lesen.
VERWEIS
6.1 6.2 6.3
Schnelleinstieg Verwendung der .NET-Bibliotheken Objektbrowser
186 193 203
Dieses Kapitel beschränkt sich auf die Nutzung von Objekten, die durch VB.NET bzw. durch die .NET-Bibliotheken vorgegeben sind. Sie können mit VB.NET natürlich aber auch eigene Klassen mit Eigenschaften, Methoden etc. programmieren. Detaillierte Informationen zur objektorientierten Programmierung mit VB.NET finden Sie im nächsten Kapitel. Eine zumeist gut verständliche Sprachdefinition finden Sie auch in der Online-Hilfe, wenn Sie nach Visual Basic Programmiersprachenspezifikation suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/vbcn7/html/vaconprogrammingwithvb.htm
186
6.1
6 Klassenbibliotheken und Objekte anwenden
Schnelleinstieg
Dieser Abschnitt führt anhand von zwei Beispielen in die objektorientierte Programmierung ein – zumindest so weit es um die Nutzung von vorgegebenen Klassen aus der .NETBibliothek geht. Als Grundlage für die beiden Beispiele dienen verschiedene Klassen, die den Zugriff auf das Dateisystem ermöglichen. Diese Klassen werden im Detail zwar erst in Kapitel 10 vorgestellt, aber Sie werden die Beispiele auch ohne die dort vermittelten Details verstehen.
6.1.1
Miniglossar
Dieses Miniglossar soll Ihnen beim Verständnis der nachfolgenden Beispielprogramme helfen. Die Kurzbeschreibungen der wichtigsten Begriffe aus der objektorientierten Programmierung sind natürlich recht theoretisch und abstrakt – aber anhand der Beispiele in den nächsten Abschnitten sollte die Bedeutung dann rasch klar werden. (Ein ausführlicheres Glossar finden Sie übrigens im Anhang dieses Buchs.) Klasse: Eine Klasse beschreibt die Eigenschaften eines Objekts. Die Klasse ist gewissermaßen der Bauplan für ein Objekt. In der .NET-Klassenbibliothek sind mehrere Tausend Klassen definiert. Damit ist die Klassenbibliothek eine wichtige Grundlage für eigene Programme. Objekte: Objekte sind konkrete Realisierungen von Klassen. In Variablen speichern Sie daher Objekte, nicht Klassen. Objekte müssen explizit mit dem New-Operator erzeugt werden, entweder bei der Deklaration in der Form Dim var As New xyz() oder zu einem späteren Zeitpunkt durch eine Zuweisung in der Art var = New xyz(). Die Variable var verweist dann auf das Objekt der Klasse xyz. Manchmal wird ein Objekt auch als Instanz einer Klasse bezeichnet. Methoden: Methoden sind mit Prozeduren oder Funktionen vergleichbar. Der Unterschied besteht darin, dass sie normalerweise auf ein Objekt angewendet werden, entweder um Daten des Objekts zu ermitteln oder um das Objekt zu bearbeiten. Es gibt auch besondere Methoden (so genannte Shared-Methoden), die direkt auf eine Klasse angewendet werden können. Derartige Methoden dienen meist zur Ermittlung von Informationen oder zur Durchführung von Operationen, die unabhängig von einem konkreten Objekt sind. Eigenschaften: Eigenschaften sind mit Variablen vergleichbar. Der Unterschied besteht abermals darin, dass Eigenschaften auf ein Objekt oder eine Klasse angewendet werden müssen. Eigenschaften dienen dazu, Daten eines Objekts zu lesen oder zu verändern. (Es gibt auch ReadOnly-Eigenschaften, die nur gelesen werden können.) Ereignisse: Ereignisse erleichtern die Kommunikation zwischen einem Objekt und dem Programm, das das Objekt erzeugt hat. Ereignisse ermöglichen es, dass bei bestimmten Änderungen des Objektinhalts automatisch eine Prozedur aufgerufen wird. (Das erspart die aus manchen Programmiersprachen bekannten Warteschleifen, in denen periodisch überprüft wird, ob sich bestimmte Daten eines Objekts mittlerweile verändert haben.)
6.1 Schnelleinstieg
187
Namensräume: Die meisten Klassennamen der .NET-Bibliothek sind ziemlich lang – z.B. System.Windows.Forms.Button. Die mehrteiligen Klassennamen ermöglichen es, inhaltlich zusammengehörende Klassen zu Gruppen zusammenzufassen. Eine derartige Gruppe ist System.Windows.Forms. Sie enthält alle zur Windows-Programmierung erforderlichen Klassen. Der offizielle Begriff für derartige Gruppen lautet Namensraum (name space).
6.1.2
Beispiel 1 – Textdatei erzeugen und löschen
Beim hier vorgestellten Beispielprogramm handelt es sich um eine Konsolenanwendung (siehe Abbildung 6.1). Als einziges Programm in diesem Buch ist es mit Zeilennummern abgedruckt, damit eindeutig auf einzelne Zeilen hingewiesen werden kann. Wenn Sie das Programm selbst eingeben, dürfen Sie die Zeilennummern natürlich nicht mit eingeben! (Einfacher ist es ohnedies, das Programm von der beiliegenden CD zu laden.)
Abbildung 6.1: Beispielprogramm zur objektorientierten Programmierung
Die Programmausführung beginnt und endet mit der Prozedur Main, die (der einzige) Bestandteil des Moduls Module1 ist. Dieser Programmaufbau ist charakteristisch für die meisten Konsolenanwendungen. In den Zeilen 6-9 werden mehrere Variablen deklariert. fname und txt dienen zur Speicherung von Zeichenketten. In sw soll später ein Objekt der Klasse System.IO.StreamWriter gespeichert werden. Die Kurzschreibweise IO.StreamWriter ist deswegen zulässig, weil der Compiler bei allen Klassen automatisch System voranstellt, wenn es die Klasse unter dem angegebenen Namen nicht erkennt. (Das Voranstellen von System ist Teil des so genannten Importmechanismus, der in Abschnitt 6.2.2 noch näher beschrieben wird.) Die drei im Beispielprogramm vorkommenden System.IO.*-Klassen sind Teil der Bibliothek mscorlib.dll, die allen .NET-Programmen automatisch zur Verfügung steht. Es sind daher keine besonderen Vorbereitungen erforderlich, damit die Klassen verwendet werden können. (.NET stellt noch viel mehr Bibliotheken zur Verfügung. Wenn Sie Klassen daraus benutzen möchten, müssen Sie in der Regel zuerst einen Verweis auf die Bibliothek einrichten – siehe Abschnitt 6.2.1.) Alle drei System.IO.*-Klassen befinden sich im Namensraum System.IO.
188
01 02 03 04 05 06 07 08 09
6 Klassenbibliotheken und Objekte anwenden
' Beispiel oo-programmierung\intro Option Strict On Module Module1 Sub Main() ' diverse Variablen deklarieren Dim fname, txt As String Dim sw As IO.StreamWriter Dim fi As IO.FileInfo Dim sr As IO.StreamReader
HINWEIS
In Zeile 11 wird der Dateiname einer temporären Datei ermittelt. Dazu wird die Methode GetTempFileName der Klasse System.IO.Path verwendet. Bei dieser Methode handelt es sich um eine so genannte Shared-Methode. Das bedeutet, dass es nicht erforderlich (und in diesem Fall auch gar nicht möglich) ist, zuerst ein Objekt der Klasse System.IO.Path zu erzeugen, um die Methode dann darauf anzuwenden. (Shared-Methoden werden in Abschnitt 6.2.4 genauer behandelt.) Aus unerfindlichen Gründen liefert GetTempFileName den Dateinamen in einer Kurzschreibweise, die gewährleistet, dass jeder einzelne Datei- oder Verzeichnisname maximal acht Zeichen lang ist. Diese Kurzschreibweise wurde beim Übergang von DOS bzw. Windows 3.1 zu Windows 95 eingeführt und hatte damals durchaus ihre Berechtigung. Aber warum auch die .NET-Klassenbibliothek noch zum seligen DOS kompatibel sein muss, dass weiß allein Microsoft. (Davon, dass eine DOSkompatible .NET-Version geplant ist, habe ich auf jeden Fall noch nichts gehört ...)
Zeile 12 verwendet die Methode WriteLine der Klasse System.Console, um eine Zeile in das Konsolenfenster zu schreiben. Diese Methode nimmt beliebig viele Parameter entgegen. Der erste Parameter ist zumeist eine Zeichenkette, die auch Formatplatzhalter für die weiteren Parameter enthalten darf. {0} bedeutet, dass an dieser Stelle der Inhalt des nächsten Parameters in Textform ausgegeben werden soll. Auch WriteLine ist eine Shared-Methode – deswegen ist es nicht notwendig, vorher ein Console-Objekt zu erzeugen. 10 11 12
' Dateinamen für temporäre Datei ermitteln fname = IO.Path.GetTempFileName() Console.WriteLine("Temporäre Datei: {0}", fname)
In Zeile 14 wird zum ersten Mal ein Objekt erzeugt: Dazu wird an den so genannten NewKonstruktor der Klasse System.IO.StreamWriter ein Dateiname übergeben. Diese Klasse hilft beim Schreiben von Textdateien. Der Konstruktor liefert als Ergebnis ein Objekt dieser Klasse. sw gilt nun als Objektvariable, d.h., sie verweist auf das Objekt und ermöglicht so deren Bearbeitung. (Sollte es aus irgendeinem Grund nicht möglich sein, die Datei zu erstellen, würde in dieser Zeile ein Fehler auftreten.) In den Zeilen 15-16 wird die Methode WriteLine auf das StreamWriter-Objekt angewendet. Damit werden zwei Textzeilen in der Datei gespeichert. In Zeile 17 wird die Datei durch die Methode Close geschlossen. Damit wird das StreamWriter-Objekt aus dem Speicher entfernt. (Bei manchen Klassen gibt es statt Close ein Dispose-
6.1 Schnelleinstieg
189
Methode, um das Objekt zu löschen. Die überwiegende Mehrheit der Klassen kennt aber weder Close noch Dispose. Bei derartigen Klassen gibt es keine Möglichkeit, den durch das Objekt belegten Speicher explizit freizugeben. Stattdessen erkennt die so genannte garbage collection, die ständig im Hintergrund ausgeführt wird, wenn das Objekt nicht mehr benötigt wird, und entfernt es dann automatisch aus dem Speicher.)
HINWEIS
13 14 15 16 17
' Textdatei erzeugen sw = New IO.StreamWriter(fname) sw.WriteLine("eine Zeile Text") sw.WriteLine("noch eine Zeile") sw.Close()
Wenn Sie längere Variablennamen verwenden (was in realen Programmen der Regelfall sein sollte, um eine bessere Lesbarkeit zu gewährleisten), ist es bisweilen lästig, diesen Variablennamen jedes Mal voranzustellen, wenn Sie eine Methode oder Eigenschaft auf das Objekt anwenden möchten. Um Ihnen diese Tipparbeit zu ersparen, können Sie mit With das Objekt fixieren. Allen Methoden und Eigenschaften, die sie auf das Objekt anwenden möchten, brauchen Sie nun nur noch einen Punkt voranstellen. With sw .WriteLine("eine Zeile Text") .WriteLine("noch eine Zeile") .Close() End With With kann nur für Objekte, nicht aber für Klassen verwendet werden. With Console
ist daher nicht erlaubt (auch wenn das bisweilen durchaus praktisch wäre). In Zeile 19 wird ein Objekt der Klasse System.IO.FileInfo erzeugt. Diese Klasse dient dazu, Informationen über eine Datei zu ermitteln. Das FileInfo-Objekt wird hier dazu verwendet, um festzustellen, ob die Datei überhaupt existiert und wie groß diese Datei ist. Diese Informationen werden mit den Eigenschaften Exists und Length ermittelt, die auf das Objekt angewendet werden. Die Ergebnisse werden mit der schon vertrauten Methode Console.WriteLine im Konsolenfenster angezeigt. 18 19 20 21
' Informationen über die erzeugte Datei anzeigen fi = New IO.FileInfo(fname) Console.WriteLine("Die Datei existiert: {0}", fi.Exists) Console.WriteLine("Dateilänge: {0}", fi.Length)
In Zeile 23 wird ein Objekt der Klasse System.IO.StreamReader erzeugt. Diese Klasse ist das Gegenstück zu StreamWriter und hilft beim Lesen von Textdateien. Zeile 23 beweist, dass neue Objekte nicht nur durch den New-Konstruktor erzeugt werden können; hier ist es vielmehr die Methode OpenText der FileInfo-Klasse, die als Ergebnis ein neues Objekt liefert. Auf das StreamReader-Objekt wird nun die Methode ReadToEnd angewendet. Diese Methode liest die Datei bis zu ihrem Ende und liefert das Ergebnis als Zeichenkette, die in der
190
6 Klassenbibliotheken und Objekte anwenden
Variablen txt zwischengespeichert wird. Der Inhalt der Variable wird mit Console.WriteLine im Konsolenfenster ausgegeben. (Es wäre natürlich ebenso möglich gewesen, Zeile 24 und 27 zu kombinieren: Console.WriteLine(sr.ReadToEnd()).) 22 23 24 25 26 27
' Inhalt der erzeugten Datei anzeigen sr = fi.OpenText() txt = sr.ReadToEnd() sr.Close() Console.WriteLine("Inhalt der Datei:") Console.WriteLine(txt)
In Zeile 29 wird die temporäre Datei wieder gelöscht. Dazu wird auf das (noch immer vorhandene) FileInfo-Objekt die Methode Delete angewendet. 28 29
' Datei löschen fi.Delete()
Damit das Konsolenfenster nicht verschwindet, bevor Sie die Textausgaben lesen können, wird mit der Methode ReadLine der Klasse System.Console auf die Eingabe einer Textzeile gewartet. ReadLine würde eigentlich die eingegebene Zeichenkette zurückgeben – aber die ist für das Programm uninteressant. Der einzige Sinn von ReadLine besteht hier darin, dass mit dem Programmende so lange gewartet werden soll, bis der Anwender Return drückt. Zeile 32 ist also ein Beispiel für den Aufruf einer Methode, die zwar einen Rückgabewert hat, der aber ignoriert wird. 30 31 32 33 34
' Programmende Console.WriteLine("Return drücken") Console.ReadLine() End Sub End Module
6.1.3
Beispiel 2 – Dateiereignisse empfangen
Das zweite Beispielprogramm überwacht das temporäre Verzeichnis des Anwenders, der das Programm startet. Jedes Mal, wenn sich in diesem Verzeichnis irgendetwas ändert (d.h., wenn eine Datei erzeugt, geändert, gelöscht oder umbenannt wird), wird dies im Konsolenfenster angezeigt (siehe Abbildung 6.2). Return beendet das Progamm. Um das Programm zu testen, starten Sie am besten dieses Programm und dann das im vorigen Abschnitt präsentierte Programm. Letzteres erzeugt innerhalb des temporären Verzeichnisses eine neue Datei und löscht diese etwas später wieder. (Wenn Sie das introevent-Programm länger laufen lassen, werden Sie feststellen, dass im temporären Verzeichnis laufend Dateien geändert werden.)
VORSICHT
6.1 Schnelleinstieg
191
Dieses Programm funktioniert nur, wenn Sie es unter Windows NT/2000/XP ausführen. (Nur diese Betriebssystemversionen können mit der hier eingesetzten .NETKlasse System.IO.FileSystemWatcher korrekt kommunizieren.) Dass die Dateinamen trotz dieser Betriebssystemanforderung in einer DOSkompatiblen 8+3-Schreibweise angezeigt werden, ist eine Besonderheit der System.IO-Klassen, die sich leider nicht abstellen lässt.
Abbildung 6.2: Ereignisse im temporären Verzeichnis des Anwenders
Die Main-Prozedur des Programms zeichnet sich durch prägnante Kürze aus: In der ersten Zeile wird ein neues Objekt der Klasse System.IO.FileSystemWatcher erzeugt. Dabei wird an den New-Konstruktor das Verzeichnis übergeben, das überwacht werden soll. Als Testobjekt wird das temporäre Verzeichnis verwendet, das mit der Shared-Methode GetTempPath der Klasse System.IO.Path ermittelt wird. In der nächsten Zeile wird die Eigenschaft EnableRaisingEvents auf True gesetzt. Damit löst das FileSystemWatcher-Objekt nun bei jeder Änderung innerhalb des angegebenen Verzeichnisses ein Ereignis aus. Die zwei weiteren Zeilen dienen dazu, dass das Programm erst durch die Eingabe von Return beendet wird. Der eigentlich interessante Teil des Programms befindet sich freilich außerhalb von Main. Zum einen wird die Variable fsw zur Speicherung des FileSystemWatcher-Objekts nicht innerhalb von Main deklariert, sondern auf Modulebene. Das ist erforderlich, damit das zusätzliche Schlüsselwort WithEvents angegeben werden darf. Dieses Schlüsselwort erleichtert die Deklaration von Ereignisprozeduren zum Empfang von Ereignissen. Die FileSystemWatcher-Klasse kennt fünf verschiedene Ereignisse, von denen im Beispielprogramm vier ausgewertet werden. Zum Empfang der Ereignisse müssen so genannte Ereignisprozeduren in den Code eingefügt werden. Diese Prozeduren werden automatisch aufgerufen, sobald ein Ereignis auftritt. Die Entwicklungsumgebung hilft Ihnen beim Einfügen von Ereignisprozeduren: Wählen Sie zuerst im linken Listenfeld den Namen einer mit WithEvents deklarierten Objektvariablen aus und suchen Sie dann im rechten Listenfeld ein Ereignis aus: Die Entwicklungsumgebung fügt dann die Zeilen Public Sub ereignisname ... und End Sub in den Code ein. Nun müssen Sie nur noch die Zeilen dazwischen eingeben.
192
6 Klassenbibliotheken und Objekte anwenden
An Ereignisprozeduren werden üblicherweise zwei Parameter übergeben, die Sie innerhalb der Prozedur auswerten können. sender gibt an, woher das Ereignis stammt. Beim Beispielprogramm ist das FileSystemWatcher die Ereignisquelle, d.h., sender verweist auf dieses Objekt. e enthält ereignisspezifische Daten. Bei den hier vorgestellten Ereignissen enthält e unter anderem den Namen der Datei oder des Verzeichnisses, die bzw. das das Ereignis ausgelöst hat (e.FullPath). Beim Renamed-Ereignis können Sie auch den ursprünglichen Namen ermitteln (e.OldFullPath) ' Beispiel oo-programmierung\intro-event Module Module1 Dim WithEvents fsw As IO.FileSystemWatcher Sub Main() fsw = New IO.FileSystemWatcher(IO.Path.GetTempPath) fsw.EnableRaisingEvents = True Console.WriteLine("Return beendet das Programm") Console.ReadLine() End Sub Public Sub fsw_Changed(ByVal sender As Object, _ ByVal e As System.IO.FileSystemEventArgs) Handles fsw.Changed Console.WriteLine("Datei {0} hat sich geändert", e.FullPath) End Sub Public Sub fsw_Created(ByVal sender As Object, _ ByVal e As System.IO.FileSystemEventArgs) Handles fsw.Created Console.WriteLine("Datei {0} wurde erzeugt", e.FullPath) End Sub Public Sub fsw_Deleted(ByVal sender As Object, _ ByVal e As System.IO.FileSystemEventArgs) Handles fsw.Deleted Console.WriteLine("Datei {0} wurde gelöscht", e.FullPath) End Sub Public Sub fsw_Renamed(ByVal sender As Object, _ ByVal e As System.IO.RenamedEventArgs) Handles fsw.Renamed
VERWEIS
Console.WriteLine("Datei {0} wurde umbenannt in {1}", _ e.OldFullPath, e.FullPath) End Sub End Module
VB.NET kennt zwei unterschiedliche Mechanismen zur Aktivierung von Ereignisprozeduren. In diesem Beispielprogramm wurde die WithEvents-Variante demonstriert. Wie Sie Ereignisse auch ohne WithEvents empfangen können, erklärt Abschnitt 7.6.
6.2 Verwendung der .NET-Bibliotheken
6.2
193
Verwendung der .NET-Bibliotheken
Ganz egal, welches Programm Sie in VB.NET entwickeln möchten – die Verwendung der .NET-Bibliotheken ist unumgänglich. Selbst elementare Merkmale der Sprache VB.NET sind in diesen Bibliotheken definiert (z.B. Datentypen wie Double, Integer oder String). Die Verzahnung zwischen VB.NET und den .NET-Bibliotheken geht so weit, dass es schlicht unmöglich ist, ein Programm ohne die beiden Basisbibliotheken mscorlib und system zu entwickeln. Da die große Bedeutung von .NET-Bibliotheken also auf der Hand liegt, gibt dieser Abschnitt eine Menge Tipps zur richtigen und effizienten Nutzung dieser Bibliotheken.
6.2.1
Verweise auf Bibliotheken einrichten
In VB.NET-Programmen können Sie nur die Klassen, Strukturen und Methoden solcher Bibliotheken nutzen, auf die Sie in Ihrem Programm verweisen. Per Default ist das (bei einer VB.NET-Konsolenanwendung) für die folgenden Bibliotheken der Fall: •
Microsoft Visual Basic.NET Runtime (Microsoft.VisualBasic.dll)
•
Basisklassenbibliothek (mscorlib.dll) mit der Definition der .NET-Datentypen sowie mit zahllosen Basisklassen und -Methoden.
•
System-Klassenbibliothek (System.dll) mit weiteren Grundfunktionen, z.B. zur Nutzung
unterschiedlicher Netzwerk- und Internet-Protokolle •
System.Data-Klassenbibliothek (System.Data.dll) zum Datenbankzugriff
•
System.XML-Klassenbibliothek (System.Xml.dll) zum Umgang mit XML-Daten
Von diesen fünf Bibliotheken stehen die ersten zwei in VB.NET-Projekten immer zur Verfügung. (Es ist nicht möglich, die Verweise darauf zu entfernen.) Alle weiteren Bibliotheken werden im Projekmappen-Explorer angezeigt und können dort entfernt werden (siehe Abbildung 6.3).
Abbildung 6.3: Verweise auf Bibliotheken im Projektmappen-Explorer
194
6 Klassenbibliotheken und Objekte anwenden
Bei Windows-Projekten kommen zwei weitere Defaultbibliotheken hinzu: •
System.Drawing-Klassenbibliothek (System.Drawing.dll) mit den Grafikklassen (GDI+)
•
System.Windows.Forms-Klassenbibliothek
(System.Windows.Forms.dll)
mit
zahllosen
Klassen zur Windows-Programmierung Wenn Sie darüber hinaus Klassen nutzen möchten, die in anderen Bibliotheken definiert sind, müssen Sie vorher mit PROJEKT|VERWEIS HINZUFÜGEN einen Verweis auf die entsprechende Bibliothek einrichten. (Beachten Sie, dass Sie mit dem VERWEISE-Dialog nicht nur .NET-Bibliotheken, sondern auch herkömmliche COM-Bibliotheken mit Ihrem Projekt verbinden können.)
6.2.2
Klassennamen verkürzen mit Imports
Mit Ausnahme einiger elementarer Funktionen befinden sich alle weiteren Schlüsselwörter, die Sie in Ihrer täglichen Programmierung benötigen, in diversen Bibliotheken.
HINWEIS
Ein Beispiel sind die arithmetischen Funktionen (Sin, Cos etc.): Diese Funktionen stehen nicht ohne weiteres zur Verfügung. Der Ausdruck Sin(x) liefert nur die Fehlermeldung, dass Sin unbekannt ist. Stattdessen lautet die neue Schreibweise System.Math.Sin(x). Die Funktion Sin (die nach der exakten Objektnomenklatur eigentlich eine Methode ist) wird durch die Bibliothek System.Math zur Verfügung gestellt. Statt System.Math.Sin(x) funktioniert auch Math.Sin(x). Der Grund besteht darin, dass System per Default vorangestellt wird (siehe den nächsten Abschnitt Import-Einstellungen in den Projekteigenschaften).
Wenn Sie in einem Programm häufig arithmetische Funktionen einsetzen, werden Sie es bald satt haben, ständig System.Math oder Math voranzustellen (ganz abgesehen davon, dass der Code damit fast unleserlich wird). Das muss aber auch gar nicht sein: Die Lösung für das Problem lautet Imports System.Math. Damit stehen alle in dieser Bibliothek definierten Funktionen direkt zur Verfügung. Die Sinusfunktion kann also wieder in der Form Sin(x) verwendet werden. Genau genommen importiert Imports keine Funktionen, Bibliotheken etc., sondern fügt einfach nur den angegebenen Namensraum (namespace) an. Wenn Sie ein Schlüsselwort eingeben, sucht die Entwicklungsumgebung in allen derartigen Namensräumen, ob es das Schlüsselwort darin finden kann. Die Imports-Anweisung muss am Beginn einer Codedatei (am Beginn eines Moduls) angegeben werden, und zwar vor der Deklaration von Prozeduren, Klassen etc. Die Anweisung gilt für den gesamten Code der Datei (aber nicht für das gesamte Projekt).
6.2 Verwendung der .NET-Bibliotheken
195
Imports System.Math Module Module1 Sub Main() Dim d1, d2 As Double d1 = 1.5 d2 = Sin(d1) MsgBox("Der Sinus von " & d1 & " beträgt " & d2 & ".") End Sub End Module
Import-Einstellungen in den Projekteigenschaften Neben den Imports-Anweisungen, die immer nur für eine Codedatei gelten, können Sie auch globale Importe durchführen. Um die Importe anzusehen bzw. zu verändern, klicken Sie im Projektmappen-Explorer den Projektnamen an. Anschließend finden Sie entsprechende Einstellmöglichkeit im Dialogblatt PROJEKT|EIGENSCHAFTEN|ALLGEMEINE EIGENSCHAFTEN|IMPORTE (siehe Abbildung 6.4). Um dem Projekt einen zusätzlichen Namensraum hinzuzufügen, geben Sie dessen Namen im Textfeld NAMESPACE ein und klicken dann den Button IMPORT HINZUFÜGEN an.
Abbildung 6.4: Globaler Namensraum-Import für das gesamte Projekt
HINWEIS
196
6 Klassenbibliotheken und Objekte anwenden
Es gibt weder einen Auswahldialog zur Eingabe des Namensraums, noch erfolgt eine Kontrolle, ob der Namensraum korrekt geschrieben wurde. Wenn Sie also irrtümlich Systems.Math (statt korrekt System.Math) angeben, aktzeptiert der Dialog diese Einstellung ohne Fehlermeldung. Die Verwendung der System.Math-Schlüsselwörter klappt aber natürlich nicht, bis Sie den Tippfehler entdeckt haben.
Abbildung 6.4 zeigt die Default-Importe für VB-Konsolenprojekte. Je nach Projekttyp gelten aber andere Importe! Bei Windows-Anwendungen sind beispielsweise auch System.Drawing und System.Windows.Forms aktiviert. Intern werden diese Einstellungen in der Projektdatei (Endung *.vbproj) gespeichert.
TIPP
Die Defaulteinstellungen (je nach Projekttyp) sind der Grund, weswegen Sie in Programmen viele Schlüsselwörter unmittelbar nutzen können, obwohl diese Schlüsselwörter in den unterschiedlichsten Bibliotheken, Klassen und Namensräumen definiert sind. Natürlich tritt manchmal auch das umgekehrte Problem auf: Sie haben sich daran gewöhnt, dass Sie bestimmte Schlüsselwörter ohne Imports-Anweisungen in Ihren Programmen nutzen können. In einem anderen Projekt funktioniert das dann plötzlich nicht mehr. Die Ursache ist fast immer, dass in den Projekteigenschaften andere Defaulteigenschaften gelten. Aus diesem Grund verwenden die meisten Beispiele dieses Buchs keine Importe außer den Defaultimporten der Entwicklungsumgebung.
Doppeldeutigkeiten (Namenskonflikte) In unterschiedlichen Namensräumen können gleichnamige Klassen oder Methoden definiert sein. Wenn Sie unter Zuhilfenahme von Importen die Kurzschreibweise verwenden, kann es zu Doppeldeutigkeiten kommen, die der Compiler nicht automatisch korrekt auflösen kann. Beispielsweise kennt die Klasse System.Windows.Forms.Form die Eigenschaft Left, die die xKoordinate des linken Fensterrands enthält. Dem steht die Methode Left aus der Klasse Microsoft.VisualBasic.Strings gegenüber. Wenn Sie nun im Formularcode eines Windows-Programms die Zeichenkettenmethode Left wie gewohnt unmittelbar verwenden möchten (s = Left("abcd", 2)), kommt es zu einem Fehler: Der Compiler beklagt sich darüber, dass die Eigenschaft Left gar keine Parameter
akzeptiert. (Offensichtlich ist der Compiler davon überzeugt, dass Sie die Windows-Eigenschaft Left meinen.) Um dieses Problem zu umgehen, müssen Sie den Namen der gewünschten Left-Methode exakter angeben (also z.B. mit s = Strings.Left("abcd", 2)).
Alias Sie können mit Imports auch eine Abkürzung, also einen so genannten Alias definieren. Die beiden folgenden Zeilen demonstrieren das Konzept. (Wirklich sinnvoll ist diese Vorge-
6.2 Verwendung der .NET-Bibliotheken
197
hensweise natürlich nur bei längeren Namensräumen und wenn Sie Namenskonflikte durch gewöhnliche Importe vermeiden möchten.) Imports mymath = System.Math d2 = mymath.Sin(d1)
Manchmal klappt der Zugriff auf Methoden auch ohne Imports Warum können manche Methoden direkt verwendet werden, während anderen der Klassennamen vorangestellt werden muss? Beispielsweise können Sie die Zeichenkettenfunktion Microsoft.VisualBasic.Strings.Left ohne weiteres in der Form Left(...) verwenden, während Sie der Mathematikfunktion System.Math.Sin zumindest Math voranstellen müssen (also Math.Sin(...)).
VERWEIS
Das Verhalten von Sin entspricht dem Regelfall, der für alle Klassen der .NET-Standardbibliothek gilt. Left ist insofern eine Ausnahme, als es nicht in den .NET-Bibliotheken definiert ist, sondern in einer VB-Zusatzbibliothek Microsoft.VisualBasic. Diese Bibliothek verwendet zur Deklaration mancher Klassen das Attribut . Dieses Attribut ist nicht dokumentiert, es bewirkt aber offensichtlich, dass die Klasse wie ein Modul betrachtet wird, dessen globale Methoden unmittelbar – ohne Nennung des Klassennamens – verwendet werden können. Offensichtlich soll dieses Attribut eine etwas höhere Kompatibilität zu VB6 erzielen. In Abschnitt 7.3.1 finden Sie genaue Informationen darüber, was Module sind und wodurch sie sich von Klassen unterscheiden. Eine Erklärung, was Attribute sind, folgt in Abschnitt 7.7.
6.2.3
Das System-Wirrwarr
Aus dem Namen System.begriff1.begriff2.begriff3 geht leider nicht hervor, ob begriffn eine Klasse, ein Namensraum oder eine Methode ist, wo das Schlüsselwort definiert ist und wie es in einem VB.NET-Programm verwendet werden kann. Die folgenden Beispiele illustrieren das Problem. Es geht jeweils um die Frage, wie ein Objekt der betreffenden Klasse in einem VB.NET-Programm genutzt werden kann: •
System.Random: Diese Klasse ist Teil der Bibliothek mscorlib.dll. Diese Bibliothek steht in VB.NET-Programmen immer zur Verfügung.
Innerhalb der mscorlib-Bibliothek sind mehrere so genannte Namensräume definiert. Random ist eine Klasse des Namensraums System. Ein Objekt der Klasse System.Random kann ohne weiteres mit Dim myobj As Random deklariert und verwendet werden, weil einerseits die mscorlib-Bibliothek in jedem VB.NET-Programm zur Verfügung steht und weil andererseits System zu den Defaultimporten zählt (daher die Kurzschreibweise Random statt System.Random).
198
•
6 Klassenbibliotheken und Objekte anwenden
System.Text: Hierbei handelt es sich nicht um eine Klasse, sondern um einen Namens-
raum der CLR, der selbst Klassen enthält. Es ist daher weder möglich noch sinnvoll, ein Objekt der Klasse System.Text zu erzeugen. •
System.Text.UnicodeEncoding: Die Encoding-Klasse ist eine der in System.Text enthaltenen Klassen. Die Anwendung ist ähnlich unkompliziert wie bei System.Random. Als Kurzschreibweise ist Text.UnicodeEncoding zulässig (wegen des System-Defaultimports).
•
System.Text.RegularExpression: Wenn Sie nun glauben, dass RegularExpression einfach eine weitere Klasse des System.Text-Namensraums wäre, irren Sie! System.Text.RegularExpression ist vielmehr ein weiterer Namensraum, der in der .NET-Bibliothek System definiert ist (Datei System.dll). Auch diese Bibliothek steht per Default in allen VB.NET-
Programmen zur Verfügung. •
System.Text.RegularExpression.Regex: Die Regex-Klasse ist in System.Text.RegularExpression definiert. Da die System-Bibliothek per Default in allen VB.NET-Programmen zur Verfügung steht, kann ein Regex-Objekt ohne weiteres erzeugt werden. Als Kurzschreibweise ist Text.RegularExpression.Regex zulässig.
•
System.Web.Mail.MailMessage: Die MailMessage-Klasse ist im Namensraum System.Web.Mail der .NET-Bibliothek System.Web (Datei System.Web.dll) deklariert. Per Default ist in
TIPP
VB.NET-Projekten kein Verweis auf diese Bibliothek eingerichtet. Um die Klasse verwenden zu können, müssen Sie daher zuerst diesen Verweis einrichten. Anschließend ist die Kurzschreibweise Web.Mail.MailMessage zulässig. Wenn Sie diese – nur in den ersten Tagen verwirrenden – Hintergründe selbst erforschen möchten, sollten Sie sich mit dem Objektbrowser anfreunden (siehe Abschnitt 6.3).
6.2.4
Shared- und Instance-Klassenmitglieder
Wenn Sie die Online-Hilfe zu einer beliebigen .NET-Klasse durchlesen, werden Sie bei jeder Klasse eine Members-Aufzählung finden. Dabei handelt es sich um eine Tabelle mit allen Methoden, Eigenschaften, Operatoren und anderen Schlüsselwörtern dieser Klasse. Diese Schlüsselwörter sind in verschiedene Gruppen gegliedert. Die folgenden Beispiele sollen Ihnen dabei helfen, die Dokumentation richtig zu lesen und zu verstehen.
Beispiel – Klassenmitglieder von System.DateTime System.DateTime ist eine Klasse der System-Bibliothek von .NET. Diese Klasse beschreibt einerseits den Visual-Basic-Datentyp Date. (Jede Date-Variable ist also genau genommen ein Objekt der System.DateTime-Klasse!) Andererseits stellt diese Klasse eine Menge Methoden, Eigenschaften etc. zur Verfügung, die auch losgelöst von Date-Variablen
verwendet werden können.
6.2 Verwendung der .NET-Bibliotheken
199
VERWEIS
Suchen Sie im Hilfesystem nach DateTime-Members oder verwenden Sie die folgende Adresse: ms-help://MS.VSCC/MS.MSDNVS.1031/cpref/html/frlrfsystemdatetimememberstopic.htm
Hier geht es nur um die Grundlagen der Anwendung von Klassenbibliotheken. Die DateTime-Klasse wird dabei nur als Beispiel verwendet. Wenn Sie genaue Informationen über die tatsächliche Anwendung der DateTime-Klassenmitglieder suchen, werfen Sie einen Blick in Abschnitt 8.2, wo der Umgang mit Daten und Zeiten detailliert beschrieben wird.
Wenn Sie sich die Dokumentation zu System.DateTime im Hilfesystem ansehen (siehe Abbildung 6.5), finden Sie dort eine lange Tabelle mit Schlüsselwörtern. Die folgende Tabelle nennt jeweils nur ein Mitglied aus jeder Kategorie. Beispiel: Ausgewählte Mitglieder der System.DateTime-Klasse Öffentliche Konstruktoren
New(y, m, d)
erzeugt ein neues System.DateTime-Objekt und intialisiert es mit dem Datum d.m.y: Dim d As New Date(2001, 12, 31)
Ein Konstruktor dient dazu, ein neues Objekt zu erzeugen. In VB.NET verwenden Sie den Konstruktor mit New. Bei manchen Konstruktoren können Sie dabei Parameter übergeben, um das neue Objekt gleich zu initialisieren. Öffentliche Felder MaxValue
liefert das größte zulässige Datum, das mit der System.DateTime-Klasse verarbeitet werden kann. Felder werden in diesem Buch als Klassenvariablen bezeichnet, um Konfusion mit Feldern (im Sinne des englischen Worts array) zu vermeiden. Sie enthalten oft Konstanten, die spezifisch für die Klasse, aber unabhängig vom jeweiligen Objekt sind (z.B. den größten und kleinsten Wert, der in der Klasse gespeichert werden kann, oder eine Naturkonstante wie System.Math.Pi).
Öffentliche Eigenschaften
Now
liefert die aktuelle Zeit (samt Datum). Eigenschaften können wie Klassenvariablen verwendet werden (auch wenn sie intern ganz anders realisiert sind). Sie geben Auskunft über die im Objekt gespeicherten Daten. Manche Eigenschaften können auch verändert werden, andere können nur gelesen werden (ReadOnly).
200
6 Klassenbibliotheken und Objekte anwenden
Beispiel: Ausgewählte Mitglieder der System.DateTime-Klasse Öffentliche Methoden
IsLeapYear(n)
Öffentliche Operatoren
Subtraction(d1, d2)
ermittelt die Zeitspanne zwischen zwei Daten. (Beachten Sie, dass Sie den Operator in dieser Form nicht in VB.NET nutzen können. Sie müssen stattdessen die Methode Subtract verwenden.)
Geschützte Felder, Eigenschaften, Methoden etc.
Finalize()
wird automatisch aufgerufen, wenn das DateTime-Objekt aus dem Speicher entfernt wird.
gibt an, ob n ein Schaltjahr ist oder nicht. Methoden dienen zur Bearbeitung von Objekten.
Geschützte Klassenmitgleider stehen Ihnen normalerweise nicht zur Verfügung. (Die einzige Ausnahme besteht darin, dass Sie Code für eine abgeleitete Klasse entwickeln. Weitere Informationen zu diesem Thema finden Sie im nächsten Kapitel.)
Abbildung 6.5: Die Beschreibung der DateTime-Klassenmitglieder in der Online-Hilfe
6.2 Verwendung der .NET-Bibliotheken
201
Shared- versus Instance-Mitglieder Im Hilfetext sind manche Schlüsselwörter durch ein gelbes Icon in der Form des Buchstaben S gekennzeichnet. Dieses S steht für Shared (VB.NET) bzw. static (C#). Es gibt an, ob es sich bei dem Schlüsselwort um ein Shared- oder um ein Instance-Mitglied der Klasse handelt. Der Unterschied zwischen diesen beiden Gruppen ist ausgesprochen wichtig für die korrekte Anwendung von Methoden. •
Shared-Schlüsselwörter können sowohl eigenständig als auch als Methoden bzw. Eigenschaften von System.DateTime-Objekten verwendet werden. Aus diesem Grund können Sie die Eigenschaft Now sowohl als Element der Variablen d verwenden als auch als eigenständige Eigenschaft der Klasse System.DateTime. Dim d, e As Date e = d.Now e = System.DateTime.Now e = DateTime.Now
'd und e sind Objekte der 'System.DateTime-Klasse 'd mit aktueller Zeit belegen 'd mit aktueller Zeit belegen 'Kurzschreibweise
Beachten Sie bitte, dass die drei Zuweisungen absolut gleichwertig sind. Obwohl es so aussieht, als würde der Ausdruck d.Now in irgendeiner Form d auswerten, ist dies bei Shared-Schlüsselwörtern nicht der Fall! Sie sollten sich angewöhnen, Shared-Schlüsselwörter immer nur den Klassennamen, nicht aber eine Objektinstanz voranzustellen. Zwar ist der dafür erforderliche Tippaufwand meist etwas höher, aber dafür ist Ihr Code viel besser zu verstehen. In den .NET-Klassen dominieren unter den Shared-Mitgliedern die Methoden. Da derartige Methoden verwendet werden können, ohne vorher ein Objekt der Klasse zu erzeugen, entsprechen sie in ihrer Anwendung eher herkömmlichen Funktionen als Methoden. Die folgenden Zeilen geben einige Beispiele für den Aufruf von Shared-Methoden aus unterschiedlichen Klassen. Dim b As Boolean, s As String, x As Double Dim ar() As String = {"xy", "abc", "123"} b = Date.IsLeapYear(2004) 'testet, ob 2004 ein Schaltjahr ist s = IO.Path.ChangeExtension("c:\name.txt", "bak") '--> c:\name.bak Array.Sort(ar) 'sortiert die Elemente des Felds ar x = Math.Sqrt(7) 'berechnet die Quadratwurzel von 7
•
Instance-Schlüsselwörter können dagegen nur verwendet werden, wenn sie auf ein konkretes Objekt angewendet werden. Dim d As Date d = d.AddDays(2)
'entspricht d = d + [zwei Tage]
Soweit es sich (wie bei Date) nicht um ValueType-Klassen handelt, muss das Objekt vor der Verwendung mit New erzeugt werden. In den folgenden Zeilen wird zuerst ein neues Objekt der Klasse System.Collections.Hashtable erzeugt. Anschließend wird darauf die Add-Methode angewandt, um die Geburtstage von zwei Personen zu speichern. ht("Gerhard") ermittelt das Geburtsdatum von Gerhard, wobei das eigentlich eine Kurz-
202
6 Klassenbibliotheken und Objekte anwenden
schreibweise für ht.Item("Gerhard") ist. ht.Count ermittelt, wie viele Einträge das Hashtable-Objekt enthält. (Was eine Hashtable ist, erfahren Sie in Kapitel 9.) Dim n As Integer, d As Date Dim ht As New Collections.Hashtable() ht.Add("Gerhard", #4/27/1978#) ht.Add("Susanne", #7/3/1967#) d = CType(ht("Gerhard"), Date) 'd = #4/27/1978# n = ht.Count 'n = 2
HINWEIS
Beachten Sie, dass die Klassifizierung in Shared- und Instanced-Schlüsselwörtern nicht immer logisch und bisweilen sogar vollkommen inkonsequent ist. Beispielsweise gilt die SortMethode von System.Array als Shared (also Array.Sort(ar)), während die äquivalente Sort-Methode von System.Collections.ArrayList ein Instanced-Schlüsselwort ist (also alist.Sort()). Im Objektbrowser können Sie den Typ von Klassenmitgliedern leider nur mit Mühe erkennen: bei Shared-Schlüsselwörter enthält die Deklaration das Schlüsselwort Shared (siehe Abbildung 6.7 im nächsten Abschnitt). Alle Mitglieder, die im Objektbrowser nicht als Shared gekennzeichnet sind, sind Instance-Mitglieder.
Verwendung von Instance-Klassenmitgliedern ohne Objektvariable Oben habe ich gerade erklärt, dass Instanced-Schlüsselwörter nur im Kontext einer Objektvariablen verwendet werden können. Im Regelfall bedeutet das also, dass Sie zuerst ein Objekt der entsprechenden Klasse deklarieren (Dim myobj As New klassenname()) und dann dessen Eigenschaften oder Methoden anwenden (myobj.MethodeXy). Die folgenden Zeilen erzeugen ein Objekt der Klasse System.Random und wenden darauf die Methode Next an, um eine Zufallszahl zu erzeugen. Dim i As Integer Dim myrand As New Random() i = New Random().Next 'i enthält eine Zufallszahl
Wenn Sie eine bestimmte Methode aber nur ein einziges Mal benötigen, können Sie auf die Objektvariable auch verzichten. Die beiden folgenden Zeilen sind zu den drei Zeilen oben gleichwertig. Die Zufallszahl (nicht das Random-Objekt!) wird in der Variablen i gespeichert. Das Random-Objekt wird hingegen sofort wieder verworfen. Dim i As Integer i = New Random().Next
'i enthält eine Zufallszahl
Vor allem Programmierer, die mit Java oder C++ vertraut sind, wird auch diese Vorgehensweise vertraut sein. Empfehlenswert ist sie aber nur, wenn Sie die Methode (hier Next) wirklich nur einmal benötigen. Wird die Methode dagegen in einer Schleife angewendet, sollten Sie das Random-Objekt unbedingt in einer Variablen speichern. (Andernfalls muss mit jedem Schleifendurchgang ein neues Random-Objekt erzeugt werden. Auch wenn sich VB.NET darum kümmert, dass diese Objekte automatisch wieder aus dem Speicher entfernt werden, kostet das Erzeugen und Beseitigen von Objekten unnötig Zeit.)
6.3 Objektbrowser
6.3
203
Objektbrowser
Ein unverzichtbares Hilfsmittel zur Erforschung der riesigen Klassenbibliotheken ist der Objektbrowser (ANSICHT|ANDERE FENSTER|OBJEKTBROWSER, siehe Abbildung 6.6). Dieser Dialog liefert Informationen über die Verwendung von Methoden, Eigenschaften und Konstanten, die in den Klassen verschiedener Bibliotheken deklariert sind. Die Grundfunktion ist einfach: Im linken Dialogteil können Sie in einem hierarchischen Listenfeld zuerst eine Bibliothek (z.B. Microsoft VB.NET Runtime oder mscorlib), dann einen so genannten Namensraum und schließlich eine Klasse auswählen. Rechts werden dann alle für diese Klasse verfügbaren Schlüsselwörter (Methoden, Eigenschaften, Konstanten etc.) angezeigt. Sobald Sie eines dieser Schlüsselwörter mit der Maus anklicken, liefert der untere Dialogteil Informationen über alle Parameter des Schlüsselworts. Manchmal liefert der Browser sogar kurze Informationen über den Zweck des Schlüsselworts.
6.3.1
Tipps zur Bedienung
Gerade Einsteiger wenden sich manchmal schnell frustriert vom Objektbrowser ab, weil Sie von der der Informationsfülle überwältigt werden. Immerhin beweist der Objektbrowser, dass Ihnen unter VB.NET tatsächlich Tausende von Methoden, Eigenschaften etc. zur Verfügung stehen (von denen Sie natürlich nur einen verschwindenden Bruchteil tatsächlich brauchen). Während der Objektbrowser bis VB6 eine relativ übersichtliche Hilfe war, kann es nun schon passieren, dass man in den vielen Hierarchieebenen verloren geht, bevor man das gewünschte Element findet. Es lohnt sich aber, sich mit dem Dialog vertraut zu machen! Vielleicht helfen Ihnen dabei die folgenden Tipps weiter: •
Eine elegante Möglichkeit, diesen Dialog zu öffnen, besteht darin, ein Objekt oder eine Methode im Programmcode mit der rechten Maustaste anzuklicken und dann den Kontextmenüeintrag GEHE ZU DEFINITION auszuführen. Sie ersparen sich damit die oft langwierige Suche in der verschachtelten Objekthierarchie. (Falls Sie die VB-Tastenkürzel verwenden, führt Shift+F2 direkt in den Objektbrowser.)
•
Der Objektbrowser kann mit Esc geschlossen werden. Esc bietet damit den schnellsten Weg zurück ins Codefenster.
•
Per Default zeigt der Objektbrowser alle Schlüsselwörter in alphabetischer Reihenfolge an. Über das Kontextmenü können Sie aber auch angeben, dass die Schlüsselwörter im linken Dialogbereich PER OBJEKTTYP und im rechten Bereich PER MEMBERTYP sortiert werden. Das bedeutet, dass im linken Dialogbereich zuerst alle Klassen, dann alle Interfaces, dann alle Strukturen etc. angezeigt werden; im rechten Dialogbereich werden zuerst alle Methoden, dann die Eigenschaften etc. angezeigt. Mit anderen Worten: zusammengehörende Schlüsselwörter werden gruppiert. (Innerhalb dieser Gruppen werden die Schlüsselwörter natürlich weiterhin alphabetisch sortiert.)
204
6 Klassenbibliotheken und Objekte anwenden
•
Wenn Sie innerhalb des Objektbrowsers Text eingeben, wird das erste Schlüsselwort gesucht, das mit diesen Buchstaben beginnt. Das ermöglicht vor allem bei umfangreichen Namensräumen wie Windows.Forms oft einen rascheren Zugriff als per Maus.
•
Der Objektbrowser bietet eine Suchmöglichkeit, mit der Sie nach Schlüsselwörtern suchen können.
•
Bei vielen Klassen und Schlüsselwörtern führt F1 direkt zum Hilfetext. (F1 funktioniert im Objektbrowser zuverlässiger als im Codefenster.)
•
Mit Strg+C können Sie das Schlüsselwort in die Zwischenablage kopieren (um es anschließend in den Programmcode einzufügen). Dabei wird der vollständige Objektname kopiert (bei Abbildung 6.6 also Microsoft.VisualBasic.Strings).
•
Der Objektbrowser enthält nur Klassen von Bibliotheken, die zurzeit in Ihr Projekt eingebunden sind. Wenn Sie zusätzliche Bibliotheken einbinden möchten, führen Sie PROJEKT|VERWEIS HINZUFÜGEN aus oder klicken den Button ANPASSEN an (der dieselbe Wirkung hat).
Abbildung 6.6: Der Objektbrowser
6.3 Objektbrowser
6.3.2
205
Deklaration von Schlüsselwörtern
Wenn Sie im Objektbrowser eine Klasse im linken Bereich oder ein Klassenmitglied im rechten Bereich anklicken, wird im grauen Bereich darunter die exakte Deklaration des Schlüsselworts in der Syntax von VB.NET angezeigt (Public Shared Sub Sort(...) in Abbildung 6.7).
Abbildung 6.7: Sort ist eine Shared-Methode der Klasse System.Array
Sobald Sie einmal gelernt haben, die Informationen richtig zu interpretieren, gibt Ihnen diese Zeile genaue Auskunft darüber, wie Sie die Klasse, Methode etc. in Ihren Programmen einsetzen können. Alle Schlüsselwörter, die in der Deklarationszeile vorkommen, werden ausführlich im nächsten Kapitel beschrieben. Eine kompakte Referenz finden Sie in Abschnitt 7.10. Vorweg eine kleine Orientierungshilfe zu den wichtigsten Begriffen: •
Public, Private und Protected geben den Gültigkeitsbereich an. Bei der gewöhnlichen Anwendung können Sie nur Public-Schlüsselwörter nützen. (Protected-Schlüsselwörter stehen zur Verfügung, wenn Sie Klassen vererben.)
•
Class, Module und Structure sind verschiedene Varianten von Klassen. Structure-Klassen sind immer Werttypen (ValueType-Klassen).
•
Sub und Function bezeichnet Methoden, Property eine Eigenschaft, Dim und Const eine Klassenvariable bzw. Konstante, Event ein Ereignis. ReadOnly-Eigenschaften können nur gelesen, nicht verändert werden.
•
Shared gibt an, dass das Schlüsselwort ohne Objektinstanz verwendet werden kann (siehe Abschnitt 6.2.4).
•
[Not]Overridable gibt an, ob das Schlüsselwort durch Vererbung verändert werden kann. Für die gewöhnliche Anwendung spielt das keine Rolle.
206
6 Klassenbibliotheken und Objekte anwenden
•
MustOverride bzw. MustInherit bedeuten, dass das Schlüsselwort nicht unmittelbar genutzt werden kann (sondern in vererbten Klassen verändert werden muss).
•
ByVal und ByRef geben an, wie Parameter übergeben werden (siehe Abschnitt 5.3.4).
•
Inherits gibt an, von welcher Basisklasse die Klasse abgeleitet ist.
Abbildung 6.7 zeigt, dass Sort eine Methode (Sub) der Klasse System.Array ist. Die Methode ist öffentlich zugänglich (Public). Sie kann in der Form Array.Sort(...) verwendet werden (Shared). Als Parameter muss ein Objekt des Typs System.Array (also ein beliebiges Feld) übergeben werden, das dann sortiert wird.
Abgeleitete Schlüsselwörter (Vererbung) Wenn Sie im Objektbrowser die Eigenschaften oder Methoden einer Klasse betrachten, gewinnen Sie vielleicht den Eindruck, dass manche in der Online-Dokumentation oder in diesem Buch beschriebene Schlüsselwörter ganz einfach fehlen. Der Grund dafür besteht meist darin, dass die von Ihnen betrachtete Klasse von einer anderen Klasse abgeleitet ist und von der übergeordneten Klasse manche Schlüsselwörter geerbt hat. Werfen Sie beispielsweise einen Blick auf die Schlüsselwörter der Klasse DirectoryInfo (Bibliothek mscorlib, Namensraum System.IO, siehe Abbildung 6.8): Diese Klasse gibt Auskunft über die Eigenschaften eines Verzeichnisses (z.B. C:\WinNT4\System32). Die Eigenschaft Name enthält den Namen des Verzeichnisses (z.B. "System32"). Es scheint aber keine Eigenschaft zu geben, die den gesamten Verzeichnisnamen (also inklusive dem Laufwerk und den Unterverzeichnissen gibt). Im unteren Bereich des Objektbrowsers sehen Sie, dass die Klasse DirectoryInfo von System.IO.FileSystemInfo abgeleitet ist (DirectoryInfo Inherits FileSystemInfo, siehe Abbildung 6.8). Sie können nun zu dieser Klasse springen und deren Mitglieder lesen. Noch einfacher ist es aber, die DirectoryInfo-Klasse im Objektbrowser aufzuklappen. Sie gelangen zuerst zu Basen und Schnittstellen und dann zu allen übergeordneten Klassen, von denen DirectoryInfo abgeleitet ist. Dort entdecken Sie die vermisste Eigenschaft FullName (siehe Abbildung 6.9). Dass die Eigenschaft FullName auch für Objekte des Typs DirectoryInfo zur Verfügung steht, ist eine Konsequenz des Mechanismus der Vererbung. Beachten Sie, dass derartige Vererbungsmechanismen auch über mehrere Ebenen funktionieren! Im konkreten Fall enthalten die übergeordneten Klassen MarshalByRefObject und Object weitere Klassenmitglieder, die aber nur bei der internen Objektverwaltung hilfreich sind. Diesselbe Information finden Sie übrigens auch in der Online-Hilfe. Wenn Sie einen Blick in den Hilfetext zur DirectoryInfo-Klasse werfen, sehen Sie, dass dort auch alle abgeleiteten Schlüsselwörter aufgelistet sind (siehe Abbildung 6.10).
6.3 Objektbrowser
Abbildung 6.8: Die Klasse System.IO.DirectoryInfo
Abbildung 6.9: Die übergeordnete Klasse System.IO.FileSystemInfo
207
208
6 Klassenbibliotheken und Objekte anwenden
Abbildung 6.10: Der Hilfetext zu System.IO.DirectoryInfo
6.3.3
Objektbrowser-Icons
Alle Klassen, Methoden, Funktione etc. werden im Objektbrowser durch Icons symbolisiert. Diese Icons sehen anfänglich alle gleich aus, aber mit der Zeit werden Sie merken, dass sie eine wichtige Orientierungshilfe darstellen. Die erste Tabelle enthält die Icons, die im linken Bereich des Objektbrowsers angezeigt werden (auf der Objektseite). Assembly
.NET-Bibliothek (z.B. Microsoft VB Runtime, mscorlib, System.Data)
Namespace
Namensraum (z.B. System.IO, Microsoft.VisualBasic)
Class
Klasse (z.B. System.Math, Microsoft.VisualBasic.Strings)
Module
Modul (z.B. Module1() mit Main() in einem eigenen Programm)
Structure
CLS-kompatibler Datentyp (z.B. System.Integer, System.String)
Structure
CLS-inkompatibler Datentyp bzw. Datenstruktur (z.B. System.UInt16, .TimeSpan)
Enum
Konstantenaufzählung (z.B. Microsoft.VisualBasic.MsgBoxStyle)
6.3 Objektbrowser
209
Interface
Schnittstelle (eine abstrakte Definition von Methoden und Eigenschaften, die von anderen Klassen implementiert werden; z.B. System.IFormatProvider; eine Klasse, die diesem Interface entspricht, ist etwa System.Globalization.CultureInfo)
Delegate
Beschreibung der Parameter einer Funktion oder Methode (beispielsweise beschreibt System.Windows.Forms.KeyEventHandler die Ereignisprozedur zu den Ereignissen KeyUp und KeyDown)
Objektbrowser-Icons (Mitglieder) Diese Tabelle enthält die Icons, die im rechten Bereich des Objektbrowsers angezeigt werden (auf der Mitgliederseite). Es handelt sich dabei um die Elemente von Klassen, Strukturen, Aufzählungen (Enums) etc. Sub / Function Methode (z.B. Add für Objekte der Klasse System.DateTime) Property
Eigenschaft (z.B. DayOfWeek für Objekte der Klasse System.DateTime)
Constant
Konstante (z.B. System.Math.Pi)
Dim / Const
Klassenvariable oder -konstante (z.B. Microsoft.Visual Basic.Constants.vbNullChar, System.Guid.Empty)
Gültigkeit von Schlüsselwörtern Die in den beiden vorigen Tabellen vorgestellten Icons gelten in dieser Form, wenn das Schlüsselwort öffentlich zugänglich ist. Häufig sind Klassen, Methoden etc. aber so deklariert, dass sie nur innerhalb des aktuellen Projekts oder nur bei einer Vererbung der Klasse verwendet werden können. (Ausführliche Hintergrundinformationen zu den Gültigkeitsbereichen von Schlüsselwörtern finden Sie in Abschnitt 7.9.) Diese Gültigkeitsebenen werden im Objektbrowser durch eine Erweiterung der Icons durch eine Raute, einen Schlüssel oder ein Vorhängeschloss dargestellt. Die folgende Tabelle zeigt diese Erweiterungen am Beispiel des Icons für Klassenvariablen. Analog gelten diese Erweiterungen aber auch für Eigenschaften, Methoden etc. Public Friend Protected Private
öffentliche Klassenvariable; der Zugriff auf die Variable ist immer möglich Friend-Klassenvariable; der Zugriff ist nur innerhalb des Projekts
möglich, in dem die Klasse deklariert ist Protected-Klassenvariable; der Zugriff ist nur in vererbten Klassen
möglich Private-Klasenvariable; der Zugriff ist nur innerhalb des
Klassencodes möglich, der die Klasse beschreibt
7
Objektorientierte Programmierung
Während sich das vorige Kapitel mit der Nutzung von Klassen, Objekten, Methoden und Eigenschaften beschäftigt hat, geht es in diesem Kapitel darum, selbst Klassen zu programmieren, mit Methoden und Eigenschaften auszustatten etc. Dabei werden natürlich auch Themen wie Vererbung, Schnittstellen (interfaces) Attribute etc. behandelt. Selbst wenn Sie vorerst nicht vorhaben, eigene Klassen zu programmieren, lohnt sich ein Überfliegen dieses Kapitels. Von allgemeinem Interesse ist beispielsweise der Abschnitt über die Gültigkeitsbereiche von Variablen, Prozeduren, Methoden etc. (scope). Dieses Thema wird deswegen erst am Ende dieses Kapitels behandelt, weil vorher viele der dort genannten Begriffe noch unbekannt wären. 7.1 7.2 7.3 7.4 7.5 7.6 7.7 7.8 7.9 7.10
Elemente eines Programms Klassen, Module, Strukturen Module und Strukturen Vererbung Schnittstellen (interfaces) Ereignisse und Delegates Attribute Namensräume Gültigkeitsbereiche (scope) Syntaxzusammenfassung
212 217 239 245 256 262 272 274 277 281
212
7.1
7 Objektorientierte Programmierung
Elemente eines Programms
VERWEIS
Dieser Abschnitt gibt eine erste, beispielorientierte Einführung in die Welt der Module, Klassen etc. Seien Sie beruhigt, systematische Informationen darüber, was Module, Klassen etc. sind und wie sie sich unterscheiden, folgen in den weiteren Abschnitten des Kapitels noch zuhauf! Eine zumeist gut verständliche Sprachdefinition von VB.NET finden Sie in der Online-Hilfe, wenn Sie nach Visual Basic Programmiersprachenspezifikation suchen. Dort finden Sie auch eine Beschreibung aller objektorientierten Schlüsselwörter. Diese gleichsam offizielle Sprachreferenz ist eine gute Ergänzung zu diesem Kapitel. ms-help://MS.VSCC/MS.MSDNVS.1031/vbcn7/html/vaconprogrammingwithvb.htm
Hello World als Modul Die denkbar einfachste Variante liegt bei einer Konsolenanwendung im Stil von Hello World vor. Der gesamte Code befindet sich in einer einzigen Datei module1.vb. Darin ist das Modul Module1 definiert. Es enthält eine Prozedur, nämlich Main. Die Programmausführung beginnt und endet mit dieser Prozedur. (Der Startpunkt eines Programms kann in den Projekteigenschaften eingestellt werden. Das ist dann wichtig, wenn ein Programm aus mehreren Modulen besteht.) Das Konsolenprojekt oo-programmierung\hello-world besteht genau genommen aus viel mehr Dateien, die von der Entwicklungsumgebung automatisch erzeugt werden.
HINWEIS
• AssemblyInfo.vb enthält Meta-Informationen über das Programm (Copyright, Versionsnummer etc.). • hello-world.vbproj enthält eine Liste aller Codedateien sowie alle Projekteigenschaften. hello-world.vbproj.user kann zusätzliche benutzerspezifische Ergänzungen erhalten. • hello-world.sln enthält die Liste der Projekte sowie alle Eigenschaften der Projektmappe. (Einfache VB.NET-Projektmappen enthalten nur ein einziges Projekt, aber die Entwicklungsumgebung kann auch mehrere Projekte gemeinsam verwalten.) In diesem Abschnitt geht es aber nur um den reinen Code, soweit er von Ihnen selbst erstellt wird.
7.1 Elemente eines Programms
213
' Beispiel oo-programmierung/hello-world-console ' Datei module1.vb Module Module1 Sub Main() Console.WriteLine("Hello world (module)!") Console.WriteLine("Drücken Sie Return") Console.ReadLine() End Sub End Module
Hello World als Klasse Im Zeitalter der objektorientierten Programmierung ist ein Modul eigentlich etwas Altmodisches, gewissermaßen ein Relik aus alten (Visual-)Basic-Zeiten. So verwundert es denn nicht, dass C# gar keine Module kennt. Selbstverständlich können Sie Hello World auch in VB.NET als Klasse realisieren. Dabei führen mehrere Wege zum Ziel: •
Sie können ein neues Projekt als Konsolenanwendung starten, die Moduldefinition löschen und stattdessen die folgenden Zeilen eingeben. Anschließend ändern Sie in den Projekteigenschaften das Startobjekt zu Class1.
•
Sie können ein neues Projekt auch als Klassenbibliothek starten. In der Codedatei geben Sie abermals den folgenden Code ein und ändern dann in den Projekteigenschaften den Ausgabetyp zu KONSOLENANWENDUNG.
•
Sie können die unten angegebene Klasse auch einfach beim bereits vorhandenen Projekt hello-world-console in die Codedatei einfügen. (Es ist also erlaubt, in einer Codedatei mehrere Klassen oder Module zu definieren.) Damit das Programm durch die Main()-Prozedur von Class1 gestartet wird, müssen Sie in den Projekteigenschaften als Startobjekt Class1 angeben.
Wichtig bei den folgenden Zeilen ist die Kennzeichnung der Prozedur Main mit Shared. Erst dadurch kann Main ausgeführt werden, ohne dass vorher ein Objekt der Klasse Class1 erzeugt wird. Class Class1 Shared Sub Main() Console.WriteLine("Hello world (class)!") Console.WriteLine("Drücken Sie Return") Console.ReadLine() End Sub End Class
214
7 Objektorientierte Programmierung
Das Ergebnis ist in jedem Fall dasselbe: Die Programmausführung startet und endet mit Class1.Main(). Wenn Sie also einen Widerwillen gegen Module haben, können Sie alles auch mit Klassen erreichen. Daraus ergibt sich kein Vorteil, es erleichtert aber C#- oder JavaProgrammierer die Vorstellung darüber, was ein Modul eigentlich ist (siehe auch Abschnitt 7.3.1).
Programmstart bei Windows-Anwendung Während Konsolenanwendungen generell mit Main beginnen – egal, ob sich Main nun in einem Modul oder in einer Klasse befindet –, ist der Start von Windows-Programmen eine viel komplexere Angelegenheit. Der Normalfall besteht darin, dass VB.NET beim Kompilieren die folgende Anweisung zum Anzeigen des ersten Fensters des Programms einfügt: System.Windows.Forms.Application.Run(New formname())
Das bewirkt, dass ein Objekt der Klasse formname erzeugt wird. (Dieses Objekt wird am Bildschirm als Fenster sichtbar.) Gleichzeitig wird eine so genannte Nachrichtenschleife eingerichtet, die Ereignisse (z.B. Tastatureingaben) feststellt und an das Programm weiterleitet. Das Programm endet, wenn das Startfenster geschlossen wird. (Im Detail wird der Start von Windows-Programmen in Abschnitt 15.2 beschrieben.)
Codedateien Ein Programm kann sich aus beliebig vielen *.vb-Codedateien zusammensetzen. Innerhalb jeder Codedatei können wiederum beliebig viele Klassen und Module definiert werden. Für die Gültigkeitsbereiche von Modulen, Prozeduren, Variablen etc. spielt es keine Rolle, in welcher Datei sich der Code befindet. Um eine neue Codedatei hinzuzufügen, führen Sie PROJEKT|MODUL HINZUFÜGEN oder PROJEKT|KLASSE HINZUFÜGEN aus. (Die beiden Kommandos sind absolut gleichwertig. Der Unterschied besteht darin, dass als Dateiname einmal ModuleN.vb vorgeschlagen wird, das andere Mal ClassN.vb. Außerdem enthält die neue Datei einmal eine leere Modulschablone, das andere Mal eine leere Klassenschablone. Das sollte aber nicht darüber hinwegtäuschen, dass Sie in jeder dieser Dateien nach Belieben Klassen und Module definieren dürfen und dass der Compiler beide Dateien vollkommen gleichwertig behandelt.) Direkt in der Codedatei können Sie die folgenden Konstrukte starten: Namespace, Module, Class, Structure, Enum, Interface
Hingegen können Deklarationen von Variablen, Konstanten, Prozeduren, Methoden, Eigenschaften und Ereignissen nur innerhalb eines Moduls, einer Klasse oder einer Struktur vorgenommen werden.
Projekteigenschaften Mit dem in Abbildung 7.1 dargestellten Dialog kommen Sie normalerweise erst dann in Berührung, wenn Sie das Defaultverhalten von VB.NET-Projekten ändern möchten. (Per Default beginnt die Programmausführung bei Konsolenanwendungen mit der Prozedur
7.1 Elemente eines Programms
215
Main des ersten Moduls, bei Windows-Anwendungen mit dem Anzeigen des ersten Fens-
ters.)
TIPP
Aber auch wenn Sie mit dem Defaultverhalten durchaus zufrieden sind, empfiehlt sich die Auseinandersetzung mit diesem Dialog. Sie lernen dann die Hintergründe eines VB.NETProgramms ein wenig besser verstehen. Deswegen werden in den folgenden Punkten einige Einstellmöglichkeiten des Dialogs kurz beschrieben. Um den Dialog zu öffnen, klicken Sie am besten den Projektnamen im PROJEKTMAPPEN-EXPLORER mit der rechten Maustaste an. Das Kontextmenü EIGENSCHAFTEN führt zum Dialog. Das Hauptmenükommando PROJEKT|EIGENSCHAFTEN funktioniert dagegen nur dann, wenn das Projekt vorher im im PROJEKTMAPPEN-EXPLORER markiert wird.
•
ASSEMBLYNAME: Dieser Punkt gibt an, wie die resultierende Programmdatei heißen soll. An diesen Namen wird noch .exe oder .dll angehängt. (Ein Assembly ist – ein wenig vereinfacht ausgedrückt – die aus einem Projekt resultierende Programm- oder Bibliotheksdatei. Beachten Sie, dass eine Assembly bei komplexen Projekten aber auch aus mehreren Dateien bestehen kann.)
•
AUSGABETYP: Dieser Punkt bestimmt die Art des Assembly. Zur Auswahl stehen KONSOLENANWENDUNG, WINDOWS-ANWENDUNG oder KLASSENBIBLIOTHEK. (Die unzähligen Pro-
jekttypen, die zu Beginn eines neuen Projekts zur Auswahl stehen, können also auf diese drei Varianten reduziert werden.) Die drei Varianten unterscheiden sich vor allem durch das Startverhalten voneinander: KONSOLENANWENDUNGEN werden normalerweise mit der Prozedur Main gestartet, WINDOWS-ANWENDUNGEN durch Application.Run(New formname()), KLASSENBIBLIOTHEKEN über-
haupt nicht. (Sie können nur von anderen Projekten verwendet bzw. getestet werden, indem dort eine Objektinstanz einer der Klassen erzeugt wird.) Ein weiterer Unterschied besteht darin, dass KLASSENBIBLIOTHEKEN zu *.dll-Dateien kompiliert werden, die beiden anderen Typen zu *.exe-Dateien. •
STARTOBJEKT: Mit diesem Listenfeld können Sie entweder SUB MAIN oder den Namen einer Klasse als Startobjekt auswählen. Im ersten Fall dürfen alle Module und Klassen des Projekts nur eine einzige Main()-Prozedur enthalten. Die Programmausführung beginnt mit dieser Prozedur.
Im zweiten Fall (Auswahl einer Klasse) wird bei Konsolenanwendungen die MainProzedur dieser Klasse gestartet. Bei Windows-Anwendungen wird eine Instanz der Klasse (die von Windows.Forms.Form abgeleitet sein muss) an Application.Run übergeben. •
STAMM-NAMESPACE: Dieses Feld bestimmt den Defaultnamensraum für das gesamte Projekt. Dieser Name ist nur dann von Bedeutung, wenn es sich bei dem Projekt um eine Klassenbibliothek handelt. In diesem Fall gibt der Namensraum an, wie im Projekt definierte Klassen von außen angesprochen werden. Wenn Sie also in Ihrem Projekt die Klasse Class1 definiert haben und der Defaultnamensraum lautet hello_world, dann kann die Klasse von externen Projekten unter dem Namen hello_world.Class1 angesprochen
216
7 Objektorientierte Programmierung
werden. (Innerhalb des Projekts besteht die Möglichkeit, weitere Unternamensräume mit dem Schlüsselwort Namespace zu definieren – siehe Abschnitt 7.8.) •
Sonstiges: In den anderen Blättern des Eigenschaftsdialogs können Sie eine ganze Menge weiterer Eigenschaften einstellen. Sie betreffen unter anderem die Genauigkeit, mit der Variablendeklarationen durchgeführt werden müssen (Option Explicit, Option Strict), das Icon der Programmdatei, Defaultimporte (siehe Abschnitt 6.2.2), Kompilier- und Debugging-Optionen etc. Diese Einstellungen sind in diesem Kapitel aber nicht von Interesse.
HINWEIS
Abbildung 7.1: Einstellung der Projekteigenschaften
Die Defaulteinstellungen für ASSEMBLYNAME und STAMM-NAMESPACE werden vom Projektnamen übernommen, den Sie beim Start eines neuen Projekts angeben. Im NAMESPACE-Namen werden gegebenenfalls unzulässige Sonderzeichen durch _ ersetzt. Achten Sie darauf, dass der Projektname nicht mit dem Namen einer .NET-Klasse oder eines .NET-Namenraums übereinstimmt – sonst gibt es Probleme beim Zugriff auf diese Klasse! Diese Probleme beheben Sie, indem Sie im Eigenschaftsdialog den NAMESPACE-Namen ändern.
7.2 Klassen, Module, Strukturen
7.2
217
Klassen, Module, Strukturen
Dieser Abschnitt führt in die Programmierung von Klassen, Modulen und Strukturen ein. Klassen stehen deswegen an erster Stelle, weil sie das universellste Konstrukt darstellen. Module und Strukturen haben ähnliche Eigenschaften und werden auf ähnliche Weise definiert (programmiert), ihr Funktionsumfang im Vergleich zu Klassen ist aber eingeschränkt. Sobald Sie verstehen, was Klassen sind und welche Merkmale sie haben, wird es Ihnen leicht fallen, auch das Konzept von Modulen und Strukturen zu erkennen.
7.2.1
Klassen
Am Beginn steht die Frage: Wozu Klassen? Nur wenn Sie wissen, wozu Sie Ihr Projekt in Klassen organisieren, kann diese Organisation auch gelingen. Im Wesentlichen helfen Klassen bei zwei Dingen: erstens, den Code in sinnvolle Einheiten zu gliedern; und zweitens, immer wiederkehrende Aufgaben so zu kapseln, dass sie sowohl im aktuellen Projekt als auch in anderen Projekten wiederverwendet werden können. Klassen sind sozusagen die Grundlage für ein modernes Code-Recycling: Statt also ein wiederkehrendes Problem immer wieder neu zu lösen, können Sie für diese Aufgabe eine Klasse entwickeln und diese dann in verschiedenen Projekten entweder als Code (durch simples Einfügen) oder als Bibliothek (durch eine Referenz) nutzen.
HINWEIS
Damit dieses Konzept erfolgreich ist, sollten Sie sich Zeit für die richtige Organisation der Klasse nehmen, d.h. für die Überlegung, durch welche Eigenschaften und Prozeduren Sie die Funktionen der Klasse nach außen hin zugänglich machen, wie die Daten intern verwaltet werden etc. Dabei können Sie durchaus die Erfahrungen, die Sie mit der Anwendung der zahllosen .NET-Bibliotheken bereits gemacht haben, in das Design mit einfließen lassen. Und vergessen Sie nicht, Ihre Klasse ordentlich zu dokumentieren! Die ganze Idee der Wiederverwendung von Code scheitert oft daran, dass es weniger Arbeit bereitet, eine Funktion ein zweites Mal neu zu implementieren als nachzuvollziehen, wie eine bereits vorhandene Klasse eingesetzt werden kann. Mit diesen beiden Absätzen schließe ich das Thema Klassendesign aus Platzgründen auch schon wieder ab. Es gibt zahllose exzellente Bücher zu den Themen Entwurfsmuster (design patterns) und Modellierung (UML, Unified Modeling Language), die sich diesem Thema in aller Ausführlichkeit widmen. In diesem Kapitel geht es lediglich darum, Ihnen die Syntaxelemente von VB.NET zu beschreiben. Mit diesem Wissen sollten Sie dann in der Lage sein, die Design-Tipps aus anderen Büchern zur objektorientierten Programmierung in VB.NET umsetzen.
Was sind Klassen und woraus bestehen sie? Eine Klasse ist die abstrakte Beschreibung (der Bauplan) eines objektorientierten Datentyps. Die Schnittstelle nach außen (also zur Anwendung der Klasse) wird in erster Linie
218
7 Objektorientierte Programmierung
durch Eigenschaften und Methoden hergestellt. Viele Klassen bieten auch den direkten Zugang auf Datenfelder und Konstanten. Einige Klassen kennen darüber hinaus Ereignisse, die beispielsweise dann ausgelöst werden, wenn sich Daten auf eine bestimmte Weise ändern.
Definition von Klassen Auf der Codeebene wird die Definition einer Klasse durch Class name eingeleitet und durch End Class abgeschlossen. Innerhalb dieses Blocks werden die Elemente der Klasse definiert – also Variablen (alias Datenfelder), Prozeduren (alias Methoden), Eigenschaften etc. Class Class1 Private x As Integer Public y As Integer Private Sub p() ... Sub m() ... Property e() As Integer ... End Class
'eine 'eine 'eine 'eine 'eine
interne Klassenvariable öffentliche Klassenvariable interne Prozedur von außen zugängliche Methode von außen zugängliche Eigenschaft
VERWEIS
Entscheidend beim Entwurf der Klasse ist die Überlegung, welche Elemente der Klasse nur für die intere Programmierung innerhalb der Klasse gedacht sind und welche Elemente extern zur Verfügung stehen sollen. Dabei müssen Sie auf die korrekte Deklaration achten. Beispielsweise gelten Variablen per Default als Private und können nur intern verwendet werden, während Prozeduren (Methoden) und Eigenschaften per Default als Public gelten und von außen hin verwendet werden können. Die möglichen Gültigkeitsebenen von Variablen, Prozeduren etc. und die Schlüsselwörter zur entsprechenden Deklaration (Private, Friend, Protected, Public) werden in Abschnitt 7.9 ausführlich beschrieben.
Anwendung von Klassen (Objekte) Wenn Sie die Definition einer Klasse abgeschlossen haben, können Sie die Klasse anwenden. Im Regelfall erzeugen Sie dazu ein Objekt dieser Klasse: Dim o As New Class1() o.y = 3 o.e = 4 o.m()
'Objekt der Klasse Class1 erzeugen 'öffentliche Klassenvariable nutzen 'Eigenschaft zuweisen 'Methode aufrufen
VERWEIS
7.2 Klassen, Module, Strukturen
219
Die Nutzung von Klassen – d.h. der Umgang mit Objekten – wird ausführlich in den Kapiteln 4 und 6 zur Variablenverwaltung und zur Anwendung von Klassenbibliotheken und Objekten beschrieben.
Verschachtelung von Klassen Klassendefinitionen können ineinander verschachtelt werden (siehe die folgende Schablone). Das bewirkt, dass Sie Class2 innerhalb von Class1 unmittelbar verwenden können. In anderen Klassen können Sie Class2 dagegen nur unter dem vollständigen Namen Class1.Class2 ansprechen. (Unter diesem Namen scheint Class2 auch im Objektkatalog auf.) Class Class1 ' Class2 nutzen Dim c As New Class2() ' ... weiterer Code für Class1 Class Class2 ' ... weiterer Code für Class2 End Class End Class ' Class1.Class2 nutzen Class Class3 Dim c As New Class1.Class2() ' ... weiterer Code für Class3 End Class
LinkedList-Beispiel
HINWEIS
Damit dieses Kapitel nicht vollkommen abstrakt bleibt, wird zur Beschreibung der Elemente einer Klasse (Methoden, Eigenschaften etc.) ein durchgängiges Beispiel verwendet. Ziel der Klasse LinkedList ist es, eine Liste von Zeichenketten so zu verwalten, dass an einer beliebigen Stelle innerhalb der Liste Elemente eingefügt und wieder entfernt werden können. Intern verweist jedes Objekt auf das nächste bzw. vorangehende Objekt. Um die Verwaltung der Liste zu vereinfachen, wird die Klasse mit Methoden wie Insert oder Remove ausgestattet. Beachten Sie bitte, dass dieses Beispiel in erster Linie didaktischer Natur ist. Wenn es Ihnen darum geht, eine Liste von Zeichenketten effizient zu verwalten, sollten Sie nicht diese Beispielklasse, sondern die viel leistungsfähigere Klasse Collections.ArrayList verwenden (siehe Kapitel 9)!
220
7 Objektorientierte Programmierung
7.2.2
Klassenvariablen und -konstanten (fields)
Wenn Sie innerhalb einer Klasse Variablen oder Konstanten öffentlich deklarieren (mit Public), können deren Werte direkt gelesen und verändert werden. Nach außen hin wirken solche Variablen wie Eigenschaften, aber im Objektbrowser wird unmissverständlich klar, dass es sich um Klassenvariablen bzw. -konstanten handelt. (Der Unterschied zu richtigen Eigenschaften wird in Abschnitt 7.2.4 beschrieben.) Natürlich können Sie Variablen bzw. Konstanten auch mit Private deklarieren – dann können Sie auf die Variablen nur im Code innerhalb der Klasse (also z.B. in einer Methode) zugreifen. Class Class1 Public data As Integer Private internaldata As String ... End Class
Syntaktisch ist es auch erlaubt, eine Variable als ReadOnly zu deklarieren (z.B. Public ReadOnly abc As Integer = 3). Praktisch ist das aber selten sinnvoll – Sie können derartige Variablen weder innerhalb noch außerhalb der Klasse verändern (außer durch die Zuweisung im Rahmen der Deklaration). Daher ist es klarer, derartige Variablen gleich als Konstante zu deklarieren.
LinkedList-Beispiel Die Grundidee des LinkedList-Beispiels, das in den folgenden Abschnitten schrittweise erweitert wird, ist einfach: Jedes Element einer derartigen Liste wird durch ein eigenes LinkedList-Objekt dargestellt. In der Minimalvariante lässt sich eine derartige Datenstruktur durch eine Klasse darstellen, die aus nur drei Klassenvariablen besteht: Value enthält die zu speichernde Zeichenkette. Next- und PreviousItem verweisen entweder auf nachfolgende bzw. vorausgehende LinkedList-Elemente, sie enthalten Nothing, wenn das Objekt am Anfang bzw. Ende der Liste steht.
HINWEIS
' Beispiel oo-programmierung\linkedlist1 Class LinkedList Public NextItem As LinkedList Public PreviousItem As LinkedList Public Value As String End Class
Grundsätzlich wäre es auch möglich, die gesamte Liste nicht durch viele, sondern durch ein einziges LinkedList-Objekt darzustellen. Die Verwaltung der Elemente würde dann innerhalb der Liste erfolgen. Das ist mit diversen Vor- und Nachteilen verbunden. Der Hauptvorteil der hier gewählten Variante besteht darin, dass das Konzept einfach verständlich ist und sich didaktisch gut darstellen lässt. Die Effizienz steht hier nicht an erster Stelle.
7.2 Klassen, Module, Strukturen
221
Anwendung der LinkedList-Klasse So einfach die Definition der Klasse ist, so umständlich ist deren Anwendung. Die folgenden Zeilen zeigen, wie vier LinkedList-Objekte erzeugt und initialisiert werden, so dass in jedem Element ein Wort eines kurzen Satzes gespeichert ist. Durch die Schleife am Ende von Main wird die gesamte Liste im Konsolenfenster ausgegeben. Dort können Sie den Text "Das ist ein Satz." lesen. Abbildung 7.2 zeigt die interne Darstellung der Liste. Sub Main() ' einen kurzen Satz in Form von ' LinkedList-Elementen formulieren Dim o1 As New LinkedList() Dim o2 As New LinkedList() Dim o3 As New LinkedList() Dim o4 As New LinkedList() o1.Value = "Das" o1.NextItem = o2 o2.Value = "ist" o2.PreviousItem = o1 o2.NextItem = o3 o3.Value = "ein" o3.PreviousItem = o2 o3.NextItem = o4 o4.Value = "Satz." o4.PreviousItem = o3 ' Satz ausgeben Dim item As LinkedList = o1 While Not item Is Nothing Console.Write("{0} ", item.Value) item = item.NextItem End While End Sub
Nothing
Value=“Das“
Value=“ist“
Value=“ein“
NextItem
NextItem
NextItem
NextItem
PreviousItem
PreviousItem
PreviousItem
PreviousItem
Abbildung 7.2: Vier miteinander verknüpfte LinkedList-Objekte
Value=“Satz.“ Nothing
222
7 Objektorientierte Programmierung
7.2.3
Methoden
Aus Anwendersicht dienen Methoden dazu, bestimmte Operationen mit einem Objekt durchzuführen. Aus der Sicht der Programmiererin einer Klasse ist eine Methode aber einfach eine Prozedur bzw. Funktion innerhalb einer Klasse, die öffentlich zugänglich ist. Wenn die Methode als Funktion formuliert wird, liefert sie einen Rückgabewert, sonst nicht. (In einer Klasse können selbstverständlich auch Prozeduren enthalten sein, die nur intern verwendet werden und nach außen hin unzugänglich sind.)
VERWEIS
Class Class1 ' Methode xy mit Rückgabewert Public Function xy(ByVal n As Integer) As String ... End Function ' Methode z ohne Rückgabewert Public Sub z(ByVal n As Integer) ... End Sub End Class
Ein Thema für sich ist die sinnvolle Namensgebung (natürlich nicht nur bei Methoden, sondern auch bei Klassen, Variablen, Eigenschaften etc.) In der Online-Dokumentation finden Sie eine recht hilfreiche Sammlung von Regeln, denen gemäß Sie eigene Klassen, Eigenschaften, Methoden etc. benennen sollten. Suchen Sie nach den Richtlinien für die Benennung: ms-help://MS.VSCC/MS.MSDNVS.1031/cpgenref/html/cpconnamingguidelines.htm
Me-Schlüsselwort Mit dem Schlüsselwort Me können Sie innerhalb des Klassencodes auf die aktuelle Instanz der Klasse zugreifen. Wenn die Anwenderin der Klasse also obj.methode() ausführt, dann verweist Me innerhalb von Public Sub methode() auf obj. Allzuoft werden Sie Me nicht brauchen, weil der Zugriff auf Objektvariablen, -methoden, -eigenschaften etc. innerhalb des Klassencodes ohnedies problemlos möglich ist. Me.variable und variable sind also gleichwertig. Me ist aber dann wichtig, wenn Sie eine Instanz des Objektes als Ergebnis zurückgeben oder als Parameter an eine andere Methode übergeben möchten.
New-Methode (Konstruktur) Eine Sonderrolle nehmen Methoden mit dem Namen New ein. Diese Methode ist nicht zum Aufruf in der Form obj.New(...) gedacht, sondern zur Erzeugung und Initialisierung einer neuen Objektinstanz der Klasse in der Form Dim obj As New klasse(...) bzw. obj = New klasse(...).
7.2 Klassen, Module, Strukturen
223
HINWEIS
Es ist nicht zwingend erforderlich, eine eigene Klasse mit New auszustatten. Auch wenn Sie sich dagegen entscheiden, können Sie neue Objekte durch Dim obj As New klasse() erzeugen. Allerdings ist es dann nicht möglich, eine Initialisierung durchzuführen. Wenn Sie eine eigene New-Methode mit Parametern zur Initialisierung angeben, steht der Defaultkonstruktor ohne Parameter nicht mehr zur Verfügung und Sie müssen auch diese Methode selbst programmieren (gegebenenfalls einfach durch die beiden Zeilen Public Sub New() und End Sub). Beachten Sie, dass es in VB.NET zulässig ist, mehrere Methoden mit dem gleichen Namen zu definieren, wenn sie sich durch ihre Parameterliste eindeutig unterscheiden. (Das gilt auch für gewöhnliche Prozeduren – siehe Abschnitt 5.3.4.)
Finalize-Methode (Destruktor) Das Gegenstück zu New ist die Methode Finalize. Diese Methode wird im Rahmen der garbage collection automatisch ausgeführt, wenn das Objekt aus dem Objektspeicherraum (heap) entfernt wird.
VERWEIS
Da für die Klasse Object bereits eine Default-Finalize-Methode vorgesehen ist und alle eigenen Klassen automatisch von Object abgeleitet sind, ist es im Regelfall nicht erforderlich, dass Sie eine eigene Finalize-Methode angeben! Eine eigene Finalize-Methode ist nur dann notwendig, wenn beim Entfernen eines Objekts aus dem Speicher auch Datenbankverbindungen, offenen Dateien etc. geschlossen werden müssen. In solchen Fällen sollten Sie für die Klasse auch die IDisposable-Schnittstelle implementieren. Hintergründe zur .NET-Verwaltung des Objektspeicherplatzes sind in Abschnitt 4.6 beschrieben. Beachten Sie, dass Sie Finalize nicht selbst aufrufen dürfen, sondern den Aufruf von Finalize der garbage collection überlassen müssen. Beachten Sie auch, dass Sie keinen Einfluss auf die Reihenfolge haben, in der nicht mehr benötigte Objekte aus dem Speicher entfernt werden. Wenn Sie das Entfernen von Objekten aus dem Speicher selbst in die Hand nehmen möchten, müssen Sie für die Klasse die IDisposable-Schnittstelle implementieren und für das Objekt die Methode Dispose ausführen. Die Vorgehensweise ist in Abschnitt 7.5.2 beschrieben.
Sie müssen Finalize mit den Schlüsselwörtern Protected Overrides deklarieren. Des weiteren müssen Sie innerhalb des Finalize-Codes MyBase.Finalize() aufrufen. Eine minimale Schablone für eine eigene Finalize-Methode sieht damit so aus. Protected Overrides Sub Finalize() ... eigener Code MyBase.Finalize() 'Finalize der zugrunde liegenden Klasse (Object) End Sub
VERWEIS
224
7 Objektorientierte Programmierung
Protected bedeutet, dass die Prozedur außerhalb des Klassencodes nicht aufgerufen werden darf. Overrides bedeutet, dass die Methode die Finalize-Methode der Basisklasse Object überschreibt. MyBase ermöglicht es, innerhalb einer Klasse auf gleich-
namige Schlüsselwörter einer Basisklasse zuzugreifen. Alle drei Schlüsselwörter werden in Abschnitt 7.4 noch näher vorgestellt. (Dort geht es um das Thema Vererbung.)
Finalize-Beispiel Im folgenden Beispiel werden 1000 Objekte einer einfachen Klasse erzeugt. Durch jede Zuweisung o = New Class1(...) wird die zuletzt in der Variablen o gespeicherte Objektinstanz ungültig. Damit kann das Objekt jederzeit durch eine garbage collection gelöscht werden. Es ist allerdings nicht vorherbestimmbar, wann die nächste garbage collection tatsächlich beginnt und in welcher Reihenfolge die Objekte aus dem Speicher entfernt werden. Um das zu ergründen, wird in der Finalize-Prozedur die Objektnummer im Konsolenfenster ausgegeben. Die Ausgabe des Programms sieht dann aus wie in Abbildung 7.3.
Abbildung 7.3: Ausgaben des Finalize-Beispielprogramms
Der Code des Programms ist einfach zu verstehen. Die neuen Objekte werden mit New erzeugt, wobei durch StrDup eine unterschiedlich lange Zeichenkette und mit i ein durchlaufender Zähler übergeben wird. (Die Zeichenkette hat nur den Sinn, den Speicherver-
7.2 Klassen, Module, Strukturen
225
brauch der Objekte künstlich zu vergrößern, um so .NET hin und wieder zu einer garbage collection zu motivieren.) Threading.Thread.Sleep(1000) bewirkt, dass das Programm eine Sekunde lang nichts tut. Während dieser Zeit kommt es mit großer Wahrscheinlichkeit zu einer weiteren garbage collection. Module Module1 Sub Main() Dim i As Integer Dim o As Class1 For i = 1 To 1000 o = New Class1(StrDup(i, "x"), i) Console.WriteLine("Created object {0}. ", i) Next Threading.Thread.Sleep(1000) Console.WriteLine("Return drücken") Console.ReadLine() Threading.Thread.Sleep(100) End Sub End Module Class Class1 Public data As String Public counter As Integer Public Sub New(ByVal s As String, ByVal i As Integer) data = s counter = i End Sub Protected Overrides Sub Finalize() Console.Write("Finalize counter={0}. ", counter) MyBase.Finalize() End Sub End Class
LinkedList-Beispiel In seiner zweiten Version wird das LinkedList-Beispiel schon wesentlich interessanter. Eine Reihe von Methoden machen sowohl das Initialisieren neuer Objekte als auch die Verwaltung von Listen deutlich einfacher. Die Grundidee besteht darin, die beiden Variablen nextItem und previousItem mit Private zu deklarieren und somit eine direkte (und fehleranfällige) Manipulation dieser beiden Zeiger auf nachfolgende bzw. vorausgehende Objekte zu unterbinden. Dafür helfen nun verschieden Methoden, weitere LinkedList-Objekte an ein bereits vorhandenes Objekt anzuhängen bzw. davon wieder zu entfernen. Ein wesentlicher Vorteil dieser Vorgehensweise besteht darin, dass es mit den neuen Methoden unmöglich ist, mehrere LinkedList-Objekte zirkulär zu verbinden. Durch eine direkte Veränderung von next- und previousItem wäre das dagegen sehr einfach möglich.
226
7 Objektorientierte Programmierung
Derartige Kreisverweise sind in der Praxis aber fast immer unerwünscht und würden eine Menge Zusatzcode erfordern, um mögliche Endlosschleifen bei der Auswertung der Listen auszuschließen. Der Konstruktor New ermöglicht es, ein neues LinkedList-Objekt zu erzeugen, wobei optional eine Zeichenkette zur Initialisierung des Objekts übergeben werden kann. Wenn der Konstruktor ohne Parameter aufgerufen wird, wird innerhalb der Klasse Me.New("") ausgeführt, d.h. eine leere Zeichenkette an die zweite New-Variante übergeben. Beachten Sie insbesondere den Einsatz des Schlüsselworts Me, das auf die aktuelle Instanz eines Objekts verweist und den Aufruf der Methode New innerhalb des Klassencodes ermöglicht. ' Beispiel oo-programmierung\linkedlist2 Class LinkedList Private nextItem As LinkedList Private previousItem As LinkedList Public Value As String ' Konstrukturen Public Sub New() Me.New("") End Sub Public Sub New(ByVal s As String) nextItem = Nothing previousItem = Nothing Value = s End Sub ... weitere Methoden End Class
Die beiden Methoden AddAfter und AddBefore erzeugen ein neues LinkedList-Objekt und fügen es vor bzw. nach dem aktuellen Objekt in die Liste ein. Als Parameter muss der gewünschte Inhalt des Objekts angegeben werden. Die Methoden kümmern sich um die korrekte Einstellung der previous- und nextItem-Eigenschaften, und zwar sowohl für das neu eingefügte Objekt als auch für die bereits vorhandene Liste. Am schwersten zu verstehen ist wahrscheinlich der dreizeilige If-Block, der in beiden Methoden vorkommt. Bei AddAfter wird durch die Abfrage getestet, ob es nach Me ein weiteres Listenelement gibt. Wenn das der Fall ist, verweist dieses momentan zurück auf Me. Durch das Einfügen des neuen Elements newitem muss es aber künftig zurück auf newitem verweisen. Genau das bewirkt nextItem.previousItem = newitem. (In AddBefore kümmert sich der If-Block analog um die Vorwärtsverweise des vorangehenden Objekts.)
7.2 Klassen, Module, Strukturen
227
' neues Element hinter dem vorhandenen Element einfügen Public Function AddAfter(ByVal s As String) As LinkedList Dim newitem As New LinkedList(s) newitem.previousItem = Me newitem.nextItem = nextItem If Not IsNothing(nextItem) Then nextItem.previousItem = newitem End If nextItem = newitem Return newitem End Function ' neues Element vor dem vorhandenen Element einfügen Public Function AddBefore(ByVal s As String) As LinkedList Dim newitem As New LinkedList(s) newitem.nextItem = Me newitem.previousItem = previousItem If Not IsNothing(previousItem) Then previousItem.nextItem = newitem End If previousItem = newitem Return newitem End Function
Ganz ähnlich sieht die Logik von Remove aus, um ein Element aus der Liste zu entfernen: Hier geht es zuerst darum, die Variable nextItem des vorangehenden Listenelements bzw. die Variable previousItem des nachfolgenden Listenelements so zu korrigieren, dass diese beiden Listenelemente nun direkt aufeinander verweisen (und nicht mehr auf das bisher dazwischenliegende Objekt, das durch Remove aus der Liste entfernt werden soll). Die IfTests sind notwendig, weil das zu löschende Objekt ja auch am Ende der Liste stehen bzw. ein isoliertes LinkedList-Element sein kann. Remove endet damit, dass die Variablen nextItem und previousItem gelöscht werden. Damit bleibt ein isoliertes LinkedList-Objekt übrig (wobei sein Inhalt – also .Value – noch immer
vorhanden ist). Sofern es im Programm keinen Verweis mehr auf das Objekt gibt, wird es nach einer Weile durch die automatische garbage collection aus dem Speicher entfernt. ' Element aus Liste entfernen Public Sub Remove() ' nextItem-Link des vorigen Eintrags richtig stellen If Not IsNothing(previousItem) Then If IsNothing(nextItem) Then previousItem.nextItem = Nothing Else previousItem.nextItem = nextItem End If End If
228
7 Objektorientierte Programmierung
' previousItem-Link des nächsten Eintrags richtig stellen If Not IsNothing(nextItem) Then If IsNothing(previousItem) Then nextItem.previousItem = Nothing Else nextItem.previousItem = previousItem End If End If ' Verweise des Elements löschen nextItem = Nothing previousItem = Nothing End Sub
Da die Variablen nextItem und previousItem nun als Private deklariert sind, gibt es keine Möglichkeit mehr, auf die nachfolgenden bzw. vorangehendenen Listenelemente zuzugreifen. Die beiden Methoden GetNext bzw. GetPrevious beheben diesen Mangel. (Wie der nächste Abschnitt zeigen wird, könnte dieselbe Funktion auch durch zwei Read-OnlyEigenschaften Next und Previous erreicht werden. Generell gilt, dass in manchen Fällen die Entscheidung zwischen einer Methode und einer Eigenschaft eine reine Geschmacksfrage ist.) ' nächstes/voriges Element der Liste ermitteln Public Function GetNext() As LinkedList Return nextItem End Function Public Function GetPrevious() As LinkedList Return previousItem End Function
Von anderen Klassen sind Sie es gewohnt, dass Sie mit obj.ToString() den Inhalt des Objekts in Textform ermitteln können. Für ein LinkedList-Objekt funktioniert ToString automatisch, weil die Klasse (wie alle Klassen) automatisch von der Klasse Object abgeleitet ist. Allerdings liefert die Defaultimplementierung von ToString nur den Klassennamen. Damit ToString auch für LinkedList das erwartete Resultat liefert, muss die Defaultimplementierung überschrieben werden. Genau das bewirkt das Schlüsselwort Overrides. Dank ToString können Sie bei der Anwendung der Klasse nun Console.Write(llobj) schreiben. Die Write-Methode wertet automatisch ToString aus und zeigt die Zeichenkette des Objekts im Konsolenfenster an. Public Overrides Function ToString() As String Return Value End Function
Für Testzwecke ist es praktisch, wenn nicht nur ein einzelnes Element, sondern gleich eine ganze Liste von LinkedList-Objekten ausgegeben werden kann. Genau dabei hilft die Methode ItemsText. Per Default liefert die Methode die Zeichenkette des aktuellen Objekts sowie maximal neun nachfolgender Objekte, wobei die Zeichenketten durch ein Leer-
7.2 Klassen, Module, Strukturen
229
zeichen voneinander getrennt werden. Durch die beiden optionalen Parameter max und delimitor können Sie die Anzahl der Zeichenketten und das Trennzeichen steuern. Public Function ItemsText(Optional ByVal max As Integer = 10, _ Optional ByVal delimitor As String = " ") As String Dim i As Integer Dim tmp As String Dim item As LinkedList = Me While (Not IsNothing(item)) And (i < max) tmp += item.Value + delimitor item = item.GetNext() i += 1 End While Return tmp End Function
Anwendung der LinkedList-Klasse Mit diesem schon recht reichen Satz an Methoden lässt sich schon ganz gut experimentieren. In den drei ersten Zeilen von Main wird dieselbe LinkedList-Kette wie im vorigen Abschnitt zusammengesetzt. Beachten Sie, dass AddAfter jeweils ein LinkedList-Element zurückgibt, auf das dann die nächste AddAfter-Methode angewendet wird. Die zweite Codezeile ist daher eine Kurzfassung der folgenden Zeilen: ' Beispiel oo-programmierung\linkedlist2 Dim a, b, c As LinkedList a = ll1.AddAfter("ist") b = a.AddAfter("ein") c = b.AddAfter("Satz.")
Statt mit AddAfter kann dieselbe LinkedList-Kette natürlich auch mit AddBefore zusammengesetzt werden. Diese Rückwärtsformulierung ist aber weniger inituitiv. Die verbleibenden Zeilen zeigen die Anwendung von GetNext und Remove. Zuerst wird nach dem Wort ist ein zusätzliches Listenelement mit dem Text neuer eingefügt (woraus sich der Satz "Das ist ein neuer Satz" ergibt). Das neue Listenelement wird anschließend wieder entfernt. Sub Main() ' einen kurzen Satz in Form von ' LinkedList-Elementen formulieren ' liefert 'Das ist ein Satz.' Dim ll1 As New LinkedList("Das") ll1.AddAfter("ist").AddAfter("ein").AddAfter("Satz.") Console.WriteLine(ll1.ItemsText())
230
7 Objektorientierte Programmierung
' liefert ebenfalls 'Das ist ein Satz.' ' ll1 --> Satz ' ll2 --> Das Dim ll2 As LinkedList ll1 = New LinkedList("Satz.") ll2 = ll1.AddBefore("ein").AddBefore("ist").AddBefore("Das") Console.WriteLine(ll2.ItemsText()) ' ll3 --> neuer ' liefert 'Das ist ein neuer Satz.' Dim ll3 As LinkedList ll3 = ll2.GetNext().GetNext().AddAfter("neuer") Console.WriteLine(ll2.ItemsText()) ' liefert wieder 'Das ist ein Satz.' ll3.Remove() Console.WriteLine(ll2.ItemsText()) End Sub
7.2.4
Eigenschaften
Eigenschaften sehen nach außen hin wie Klassenvariablen aus. Intern handelt es sich aber um ein Paar von Prozeduren. Diese Prozeduren werden beim Lesen oder Verändern der Eigenschaft ausgewertet. Die Syntax von Eigenschaften lässt sich am einfachsten anhand eines Beispiels beschreiben. Die Eigenschaft wird mit Property name As klasse eingeleitet. Wenn Sie jetzt in der Entwicklungsumgebung Return drücken, fügt sie automatisch die Codeschablone für die Get- und Set-Teile ein. Der Get-Teil der Eigenschaft ist für das Auslesen der Eigenschaft zuständig (also beispielsweise x = obj.prop). Dieser Teil muss mit einer Return-Anweisung enden, die den Wert der Eigenschaft zurückgibt. Der Set-Teil wird bei einer Veränderung der Eigenschaft ausgeführt (beispielsweise obj.prop = "abc"). An die Set-Prozedur wird der Parameter Value übergeben, der denselben Datentyp wie die gesamte Eigenschaft hat und den zuzuweisenden Wert enthält. Die Get- und SetBlöcke können jeweils vorzeitig durch Exit Property verlassen werden. Die folgenden Beispielzeilen zeigen, wie die nach außen hin unzugängliche Klassenvariable privatevar durch die Prozedur prop gelesen und verändert werden kann. Class Class1 Private privatevar As String Public Property prop() As String Get ... Return privatevar End Get
7.2 Klassen, Module, Strukturen
231
Set(ByVal Value As String) ... privatevar = Value End Set End Property End Class
Eigenschaften versus Klassenvariablen Statt der obigen Eigenschaft prop und der privaten Klassenvariable privatevar hätten Sie einfach eine öffentliche Variable prop deklarieren können (Public prop As String). Die Anwenderin der Klasse hätte keinen Unterschied gemerkt, aber Sie hätten etwas Zeit für die Programmierung der Eigenschaft gespart und wären zudem mit effizienterem Code belohnt worden. Wozu also Eigenschaften? •
Der wichtigste Vorteil einer Eigenschaft besteht darin, dass bei jedem Zugriff und bei jeder Veränderung Code ausgeführt wird. Damit haben Sie als Programmiererin der Klasse volle Kontrolle über jeden Zugriff. Das können Sie dazu ausnützen, Eigenschaften erst beim Lesen dynamisch zu errechnen, um bei jeder Veränderung eine Validätskontrolle durchzuführen etc.
•
Auf Eigenschaften basierende Klassen sind im Regelfall einfacher durch Vererbung zu erweitern.
Read-Only-Eigenschaften Eine Sonderform von Eigenschaften sind solche Eigenschaften, die nur gelesen, aber nicht verändert werden können. Bei der Deklaration werden solche Eigenschaften mit dem zusätzlichen Schlüsselwort ReadOnly gekennzeichnet. Der Set-Teil entfällt. Class Class1 Public ReadOnly Property prop() As String Get Return ... End Get End Property End Class
Analog zu ReadOnly existiert auch das Schlüsselwort WriteOnly, um Eigenschaften zu deklarieren, die nur verändert, aber nicht gelesen werden können. In diesem Fall entfällt der Get-Teil. WriteOnly-Eigenschaften sind aber sehr unüblich. Es ist leider nicht möglich, dieselbe Eigenschaft als Read- und WriteOnly-Eigenschaft mit unterschiedlichen Gültigkeitsebenen zu deklarieren (z.B. Public ReadOnly und Private WriteOnly).
232
7 Objektorientierte Programmierung
Eigenschaften mit Parametern Eigenschaften können ebenso wie Methoden mit Parametern ausgestattet werden (obwohl das in der Praxis eher selten vorkommt). Die folgenden Zeilen zeigen die grundsätzliche Syntax: Class Class1 Public Property para(ByVal n As Integer) As String Get Return ... End Get Set(ByVal Value As String) ... End Set End Property End Class
Defaulteigenschaften VB.NET bietet die Möglichkeit, eine Eigenschaft durch das Schlüsselwort Default als Defaulteigenschaft zu kennzeichnen. Es muss sich dabei um eine Eigenschaft mit Parametern handeln. Der Vorteil einer Defaulteigenschaft besteht darin, dass diese Eigenschaft nicht genannt werden muss. Statt obj.def(3) können Sie einfach obj(3) schreiben, was in manchen Fällen intuitiver ist. Beispielsweise kann auf eigene Aufzählungen ähnlich wie auf die Elemente eines Feldes zugegriffen werden. Class Class1 Public Default Property def(ByVal n As Integer) As String ... Get/Set wie bisher End Property End Class
LinkedList-Beispiel Die LinkedList-Klasse wird in der dritten Version dieses Beispiels um eine Reihe zusätzlicher Eigenschaften erweitert. Die Klassenvariablen und Methoden bleiben im Vergleich zur vorigen Version unverändert. Mit der Eigenschaft Count kann die Gesamtzahl der Elemente einer LinkedList-Kette ermittelt werden. Count funktioniert unabhängig vom Startpunkt, d.h., es werden alle vorangehenden und nachfolgenden Elemente durchlaufen, bis Nothing erreicht wird. (Bei sehr langen Ketten ist Count eine ineffiziente Eigenschaft! Eine effizientere Realisierung wäre nur möglich, wenn alle Listenelemente in einem einzigen Objekt verwaltet würden, d.h. nur bei einem vollkommen anderen Design der LinkedList-Klasse.) Count ist eine ReadOnly-Eigenschaft, d.h., sie kann nur gelesen, aber nicht verändert werden.
7.2 Klassen, Module, Strukturen
233
' Beispiel oo-programmierung\linkedlist3 Class LinkedList Private nextItem As LinkedList 'wie in Beispiel linkedlist2 Private previousItem As LinkedList Public Value As String [... methoden ...] 'wie in Beispiel linkedlist2 Public ReadOnly Property Count() As Integer Get Dim n As Integer = 1 Dim ll As LinkedList = Me.previousItem While Not IsNothing(ll) n += 1 ll = ll.previousItem End While ll = Me.nextItem While Not IsNothing(ll) n += 1 ll = ll.nextItem End While Return n End Get End Property ... weitere neue Eigenschaften End Class
Die Eigenschaften Next und Previous haben dieselbe Funktion wie die im vorigen Abschnitt vorgestellten Methoden GetNext bzw. GetPrevious: Sie liefern das nächste bzw. vorangehende Element der LinkedList-Kette (oder Nothing, wenn es keine weiteren Elemente mehr gibt). Next muss in eckige Klammern gestellt werden, weil es mit dem VB.NET-Schlüsselwort Next übereinstimmt. ' Zugriff auf das folgende bzw. vorhergehende Element Public ReadOnly Property [Next]() As LinkedList Get Return nextItem End Get End Property Public ReadOnly Property Previous() As LinkedList Get Return previousItem End Get End Property
234
7 Objektorientierte Programmierung
HINWEIS
Next und GetNext() bzw. Previous und GetPrevious() erfüllen dieselbe Aufgabe. Ist es
nun sinnvoller, diese Aufgabe durch eine Methode oder durch eine Eigenschaft zu realisieren? Eine schlüssige Antwort auf diese Frage gibt es leider nicht. Sehr oft kann eine bestimmte Funktion sowohl durch eine Eigenschaft als auch durch eine Methode erreicht werden. Für welche Variante Sie sich beim Design der Klasse entscheiden, ist eher eine Geschmacksfrage.
Der Code für die Eigenschaft Item ist schon etwas komplexer. Diese Eigenschaft ermöglicht den Zugriff auf das n-te nachfolgende bzw. vorausgehende Element in der Form obj.Item(3) oder obj.Item(-1). Da Item als Defaulteigenschaft deklariert ist, sind auch die Kurzschreibweisen obj(3) bzw. obj(-1) zulässig. Der Get-Teil zum Lesen eines Objekts ist einfach: Für n=0 liefert die Eigenschaft einfach Me zurück. Wenn n positiv ist, wird die Kette n Mal nach vorne verfolgt. Wenn dabei das Ende der Kette erreicht wird, liefert die Eigenschaft (ohne Fehlermeldung) Nothing zurück, sonst das gefundene Element. Analog wird bei negativem n das entsprechende vorausgehende Element ermittelt. Der Set-Teil zum Verändern eines Objekts wird aufgerufen, wenn Sie ein Element einer Kette verändern möchten: obj(3) = newobj. Die Prozedur wurde so implementiert, dass das newobj von seiner bisherigen Position in die neue Position verschoben wird. (Grundsätzlich wäre es auch denkbar gewesen, die Item-Eigenschaft so zu realisieren, dass der Inhalt des Objekts kopiert wird. Das wäre einfacher gewesen: Nach den einleitenden Codezeilen hätte die Anweisung olditem.Value = newitem.Value ausgereicht.)
VORSICHT
Die Set-Prozedur beginnt mit einem Test, ob das zu ändernde Objekt überhaupt existiert. Wenn das nicht der Fall ist, wird ein IndexOutOfRangeException-Fehler ausgelöst. Andernfalls wird das einzufügende Objekt durch newitem.Remove aus seiner bisherigen Position herausgelöst und durch die Veränderung seiner next- und previousItem-Variablen in die neue Position eingefügt. Soweit es an der neuen Position vorausgehende bzw. nachfolgende Elemente gibt, werden auch deren previous- und nextItem-Variablen richtig gestellt. Schließlich wird das Element, das sich bisher an der Position des neuen Elements befand, durch die Zuweisung von Nothing an previous- und nextItem zu einem isolierten LinkedListObjekt gemacht. (Wenn es keine Verweise mehr auf das Objekt gibt, wird es bei der nächsten Gelegenheit durch die garbage collection aus dem Speicher entfernt.) Wenn Sie obj(0) = newobj ausführen, verweist obj danach nicht mehr auf den Beginn der Kette, sondern auf das ehemalige Element obj(0), das jetzt isoliert ist. Auf den Beginn der Kette müssen Sie jetzt mit newobj zugreifen.
Default Public Property Item(ByVal n As Integer) As LinkedList Get Dim i As Integer Dim ll As LinkedList = Me
7.2 Klassen, Module, Strukturen
If n = 0 Then Return Me If n > 0 Then For i = 1 To n ll = ll.nextItem If ll Is Nothing Then Return Nothing Next Return ll Else For i = 1 To -n ll = ll.previousItem If ll Is Nothing Then Return Nothing Next Return ll End If End Get Set(ByVal Value As LinkedList) Dim olditem, newitem As LinkedList newitem = Value olditem = Me(n) ' es ist nicht möglich, ein gar nicht existierendes ' Objekt zu ändern If IsNothing(olditem) Then Throw New IndexOutOfRangeException() Exit Property End If ' olditem aus seiner bisherigen Position herauslösen newitem.Remove() ' Verweise des neuen Elements auf seine Nachbarn einrichten newitem.nextItem = olditem.nextItem newitem.previousItem = olditem.previousItem ' Verweise auf das neue Element einrichten If Not IsNothing(olditem.previousItem) Then olditem.previousItem.nextItem = newitem End If If Not IsNothing(olditem.nextItem) Then olditem.nextItem.previousItem = newitem End If ' Verweise des alten Elements entfernen olditem.nextItem = Nothing olditem.previousItem = Nothing End Set End Property
235
236
7 Objektorientierte Programmierung
Anwendung der LinkedList-Klasse Die folgenden Zeilen zeigen die Anwendung der neuen Eigenschaften. test1 ist der Ausgangspunkt für den schon aus früheren Beispielen bekannten Mustersatz, dessen Elemente gezählt, gelesen und verändert werden. test2 zeigt auf den Beginn einer Kette mit den Elementen "0", "1" etc. Durch die Zuweisung test2(3)=test2(7) wird das vierte Element der Kette durch das achte ersetzt. Die Kette wird dadurch um ein Element kürzer. ' Beispiel oo-programmierung\linkedlist3 Sub Main() Dim test1 As New LinkedList("Das") test1.AddAfter("ist").AddAfter("ein").AddAfter("Satz.") Console.WriteLine(test1.ItemsText()) '--> 'Das ist ein Satz.' Console.WriteLine(test1.Count) '--> 4 Console.WriteLine(test1.Next) '--> 'ist' Console.WriteLine(test1(2)) '--> 'ein' test1(2) = New LinkedList("EIN NEUER") Console.WriteLine(test1.ItemsText()) '--> 'Das ist EIN NEUER Satz.' Dim test2 As New LinkedList("0") Dim ll As LinkedList = test2 Dim i As Integer For i = 1 To 10 ll = ll.AddAfter(i.ToString) Next Console.WriteLine(test2.ItemsText(20)) '--> '0 1 2 3 4 5 6 7 8 9 10' test2(3) = test2(7) Console.WriteLine(test2.ItemsText(20)) '--> '0 1 2 7 4 5 6 8 9 10' End Sub
7.2.5
Shared-Klassenmitglieder
Klassenvariablen, Methoden und Eigenschaften können durch das zusätzliche Schlüsselwort Shared gekennzeichnet werden. Das bewirkt, dass diese Klassenmitglieder ohne eine Instanz der Klasse – also ohne dass vorher ein Objekt erzeugt wird – verwendet werden können (siehe auch Abschnitt 6.2.4). Im Regelfall wird Shared nur für öffentliche Klassenmitglieder verwendet (Public Shared). Die Deklaration privater Klassenmitglieder als Shared ist zwar zulässig, aber nur in seltenen Fällen sinnvoll.
HINWEIS
7.2 Klassen, Module, Strukturen
237
Das Gegenstück zu Shared (VB.NET) lautet in C# static! Das bedeutet auch, dass Static (VB.NET) und static (C#) zwei Schlüsselwörter mit vollkommen unterschiedlicher Wirkung sind. Static (VB.NET) kann zur Deklaration von Variablen in Prozeduren verwendet werden; es bewirkt, dass die Variablen in der Prozedur ihren Wert zwischen zwei Aufrufen behalten. static (C#) hat dieselbe Wirkung wie das hier beschriebene VB-NET-Schlüsselwort Shared.
Shared-Klassenvariablen Als Public Shared deklarierte Klassenvariablen haben eine ähnliche Wirkung wie globale Variablen innerhalb eines Moduls. Jedes Programm, das die Klasse nutzen kann, kann eine derart deklarierte Variable sofort lesen bzw. verändern. Dabei ist aber Vorsicht geboten! Alle Instanzen dieser Klasse teilen sich die als Shared deklarierte Variable! (Insofern ist die Variable also eine gemeinsame Variable aller Objekte dieser Klasse.) Das kann unter Umständen ungewollte Konsequenzen haben. Im folgenden Beispielprogramm ist die Variable x als globale Variable (Shared) der Klasse Class1 deklariert. In Module1.Main werden zwei Objekte dieser Klasse erzeugt (o1 und o2). Zur Initialisierung von x und y wird der New-Konstruktor verwendet. Anschließend werden x und y für beide Objekte ausgegeben. Das Ergebnis sieht so aus: o1.x=3 o2.x=3
o1.y=2 o2.y=4
Falls Sie o1.x=1 erwartet hätten, hier die Begründung: Class1.x ist eine gemeinsame Variable aller Instanzen (Objekten) dieser Klasse. Bei der Initialisierung von o2 wird x=3 ausgeführt – und damit gilt x=3 auch für alle anderen Objekte der Klasse. ' Beispiel oo-programmierung/shared-variable Module Module1 Sub Main() Dim o1 As New Class1(1, 2) Dim o2 As New Class1(3, 4) Console.WriteLine("o1.x={0} o1.y={1}", o1.x, o1.y) Console.WriteLine("o2.x={0} o2.y={1}", o2.x, o2.y) Console.WriteLine("Return drücken") Console.ReadLine() End Sub End Module
238
7 Objektorientierte Programmierung
Class Class1 Public Shared x As Integer Public y As Integer Sub New(ByVal x As Integer, ByVal y As Integer) Me.x = x Me.y = y End Sub End Class
Shared-Methoden Bei Shared-Methoden sind weniger Komplikationen zu erwarten. Derartige Methoden werden üblicherweise eingesetzt, •
um ein neues Objekt der aktuellen Klasse auf eine andere Weise als durch den NewKonstruktor zu erzeugen,
•
um Methoden zur Verfügung zu stellen, die nicht nur auf die aktuelle Instanz eines Objekts angewendet werden können, sondern auch auf andere Objekte (eventuell sogar auf Objekte einer anderen, verwandten Klasse),
•
um eine Sammlung von Hilfsfunktionen in Form einer Klasse zu kapseln. (Die Klasse Path aus dem Namensraum System.IO gibt dafür ein gutes Beispiel. Die darin enthaltenen Methoden dienen zur Manipulation von Verzeichnis- und Dateinamen und sind durchwegs als Shared deklariert. Dieser Klasse fehlt der New-Operator, d.h., es ist nicht nur nicht sinnvoll, sondern gar nicht möglich, ein Objekt dieser Klasse zu erzeugen.)
Wenn Sie selbst Shared-Methoden programmieren, müssen Sie im Code der Methode beachten, dass Me auf Nothing verweisen kann und dass sämtliche Klassenvariablen leer bzw. nicht initialisiert sein können.
Syntaxvarianten beim Aufruf von Shared-Methoden Der größte Nachteil von Shared-Methoden besteht darin, dass diese Methoden in zwei Formen aufgerufen werden können: entweder, indem der vollständige Name der Methode angegeben wird, oder, indem eine Variable der Klasse vorangestellt wird. Unter Anwendung der unten deklarierte Struktur vector3d gibt es also zwei Wege, die Vektoren v1 und v2 zu addieren und das Ergebnis in v3 zu speichern: Dim v1, v2, v3, v4 As vector3d v3 = vector3d.Add(v1, v2) 'Variante 1 v3 = v4.Add(v1, v2) 'Variante 2, gleichwertig
Bei Variante 2 kann statt v4 eine beliebige Variable des Typs vector3d verwendet werden, ohne dass sich das Ergebnis ändert! Genau darin liegt aber das Problem: Beim Lesen des Codes gewinnt man den Eindruck, die Addition hätte in irgendeiner Weise etwas mit v4 zu tun, vielleicht im Sinne von v3 = v4 + v1 + v2. Bei einer entsprechenden Deklaration von Add wäre eine derartige Addition ja durchaus möglich.
7.3 Module und Strukturen
239
Aus Sicht der Klassenprogrammiererin lässt sich dagegen nicht viel tun. Die einzige Möglichkeit besteht darin, derartige Prozeduren nicht in einer Klasse oder Struktur, sondern in einem Modul zu definieren (was aber ebenfalls mit vielen Nachteilen und einem Verlust an innerer Logik verbunden ist). Aus Sicht der Anwenderin der Klasse kann man nur empfehlen, generell und immer nur die erste Syntaxvariante zum Aufruf von Shared-Methoden oder -Eigenschaften zu verwenden. ' Beispiel oo-programmierung\structure-test Public Structure vector3d Public x As Double Public y As Double Public z As Double Public Shared Function Add(ByVal sum1 As vector3d, _ ByVal sum2 As vector3d) As vector3d
VERWEIS
Dim tmp As vector3d = sum1 tmp.Add(sum2) Return tmp End Function End Structure
Das obige Beispiel ist Teil des Structure-Beispiels aus Abschnitt 7.3.2. Shared-Methoden können also gleichermaßen in Klassen und in Strukturen definiert werden.
7.3
Module und Strukturen
Module und Strukturen sind Sonderfälle von Klassen, die sich durch diverse Einschränkungen von den eigentlichen Klassen unterscheiden. Dieser Abschnitt beschreibt die Unterschiede, wobei vorausgesetzt wird, dass Sie bereits eine Vorstellung über das Konzept von Klassen haben.
7.3.1
Module
Module sind Codeblöcke, innerhalb derer Deklarationen von Variablen, Prozeduren, Eigenschaften etc. durchgeführt werden können. Sie werden mit Module eingeleitet und enden mit End Module. Module Module1 Dim x As Integer Sub testme() .. End Sub End Module
240
7 Objektorientierte Programmierung
Module werden im Regelfall zur Erfüllung der folgenden Aufgaben eingesetzt: zur Deklaration global verfügbarer Konstanten, Variablen und Prozeduren (siehe unten),
•
als Startpunkt für ein Programm (Prozedur Main()),
•
zur Formulierung von Code, der losgelöst von Objektinstanzen ausgeführt werden kann (z.B. für die zentrale Steuerung des Programmstarts und seines Endes) und
•
wenn Code losgelöst von den Konzepten objektorientierter Programmierung formuliert werden soll. HINWEIS
•
Der Begriff Modul ist doppeldeutig. In diesem Kapitel und generell im Zusammenhang mit VB.NET meint ein Modul eine Sonderform einer Klasse. In der .NETNomenklatur (und insbesondere als Klasse von System.Reflection) bezeichnet ein Modul allerdings eine Datei einer Assembly.
Module versus Klassen Im Vergleich zu einer Klasse stellt ein VB.NET-Modul einfach nur eine Sonderform dar, die sich überwiegend durch Einschränkungen unterscheidet: •
Es ist nicht möglich, eine Instanz eines Moduls zu erzeugen. Demzufolge gibt es auch keinen New-Konstruktor. (Es spricht natürlich nichts dagegen, innerhalb eines Moduls eine Prozedur mit dem Namen New zu deklarieren. Zu empfehlen ist dies allerdings nicht, weil diese Prozedur nur Verwirrung stiften würde. New gilt immer als Private und kann nicht als Public deklariert werden.)
•
Alle Prozeduren innerhalb eines Moduls entsprechen Shared-Methoden bzw. -Eigenschaften einer Klasse, können also aufgerufen werden, ohne dass eine Objektinstanz erforderlich ist.
•
Module können weder anderen Klassen erben noch selbst vererbt werden.
•
Module können keine Schnittstellen implementieren.
•
Module müssen direkt auf unterster Ebene in der Codedatei angegeben werden. Module können nicht verschachtelt werden, und Module können auch nicht innerhalb von Klassen definiert werden. Umgekehrt ist es aber zulässig, innerhalb eines Moduls eine Klassendefinition durchzuführen.
Globale Variablen und Prozeduren Der einzige Punkt, in dem Module Klassen überlegen sind, besteht in der Möglichkeit, Variablen oder Prozeduren so global zu definieren, dass sie ohne die zusätzliche Angabe des Modulnamens verwendet werden können. Das bedeutet, dass Variablen, die in Modulen als Public oder Friend deklariert werden, in allen anderen Klassen und Modulen
7.3 Module und Strukturen
241
unmittelbar verwendet werden. Ebenso können alle in Modulen definierten Prozeduren, sofern sie nicht als Private gekennzeichnet sind, in allen anderen Klassen und Modulen aufgerufen werden. (Prozeduren gelten per Default als Public.) ' Deklaration der globalen Variable x und ' der globalen Prozedur global_proc Module Module1 Friend x As Integer = 4 Friend Sub global_proc() ' Code für test_proc End Sub End Module
VERWEIS
Eine detaillierte Beschreibung der Gültigkeitsebenen (des so genannten scopes) von Variablen, Prozeduren etc. sowie der Schlüsselwörter Private, Public und Friend finden Sie in Abschnitt 7.9.
HINWEIS
' globale Variable x auswerten, Prozedur global_proc aufrufen Class Class2 Sub test_proc() Console.WriteLine(x) global_proc() End Sub End Class
Wenn in zwei Modulen derselbe Variablen- oder Prozedurname verwendet wird, dann muss in anderen Modulen oder Klassen der Modulname mit angegeben werden – also etwa die Variablenbezeichnung Module2.x.
Vielleicht ist der Unterschied zwischen Modulen und Klassen noch nicht ganz klar: Sie können auch in Klassen globale Variablen oder Prozeduren (die dann Methoden heißen) definieren. Dazu müssen Sie aber das Schlüsselwort Shared verwenden, damit auf die Variablen bzw. Prozeduren sofort zugegriffen werden kann, ohne vorher eine Instanz des Objekts zu erzeugen. Und im Unterschied zu Modulen müssen bei der Nutzung dieser Variablen oder Prozeduren nun immer auch der Klassenname angegeben werden – also Class1.x oder Class1.global_proc(). (Die folgenden Zeilen entsprechen dem obigen Beispiel.) ' Deklaration der globalen Klassenvariable x und ' der globalen Methode global_proc Class Class1 Friend x As Integer = 4 Friend Shared Sub global_proc() ' Code für test_proc End Sub End Class
242
7 Objektorientierte Programmierung
VORSICHT
' globale Klassenvariable x auswerten, ' globale Methode global_proc aufrufen Class Class2 Sub test_proc() Console.WriteLine(Class1.x) Class1.global_proc() End Sub End Class
Die Verwendung von Klassenvariablen, die als Shared deklariert sind, kann unerwartete Nebenwirkungen haben (siehe auch Abschnitt 7.2.5). Verwenden Sie Shared nur, wenn Sie sich über die Bedeutung dieses Schlüsselworts im Klaren sind!
7.3.2
Strukturen
Strukturen sind Codeblöcke, in denen Deklarationen von Variablen, Prozeduren, Eigenschaften etc. durchgeführt werden können. Sie werden mit Structure eingeleitet und enden mit End Structure. (Anders als in VB6 und in vielen anderen Programmiersprachen dürfen Strukturen Code enthalten! Strukturen dienen also nicht mehr einfach dazu, ein paar elementare Variablen zu einer Gruppe zusammenzufassen!) Strukturen werden üblicherweise dazu verwendet, einfache Datenstrukturen zu definieren. Einfach meint in diesem Zusammenhang, dass der Speicheraufwand für die Daten gering ist und dass die Methoden bzw. Eigenschaften zur Initialisierung bzw. Verarbeitung der Datenstruktur in der Regel nur ganz grundlegende (oft simple) Aufgaben erledigen. Structure Structure1 Private x As Double Public y As Double Sub m() ... Property e() As Integer ... End Structure
'ein internes Element der Struktur 'ein öffentliches Element der Struktur 'eine Methode für die Struktur 'eine Eigenschaft
Strukturen versus Klassen Im Vergleich zu einer Klasse stellt eine Struktur eine Sonderform dar. Die folgenden Punkte fassen die wichtigsten Unterschiede zusammen. •
Strukturen werden automatisch von ValueType abgeleitet, d.h., es handelt sich immer um Werttypen. Daraus folgt, dass die Daten von Strukturen am Stack (und nicht im so genannten heap für Objekte) gespeichert werden. Das macht Strukturen deutlich effizienter als Klassen, solange kleine Datenmengen verwaltet werden. (Eine ausführliche Diskussion der zahlreichen Unterschiede zwischen gewöhnlichen Klassen und Werttypen finden Sie in den Abschnitten 4.1.2 und 4.6.)
7.3 Module und Strukturen
243
Beachten Sie, dass es laut Objektbrowser Strukturen der .NET-Bibliotheken gibt, die nicht von ValueType abgeleitet sind. Beispielsweise bezeichnet der Objektbrowser String als eine Struktur, die direkt von Object abgeleitet ist. Im Gegensatz dazu ist die Hilfe zu String der Ansicht, dass es sich bei String um eine NotInheritable Public Class handelt. Diese Information erscheint glaubwürdiger und stimmt mit der Auswertung des TypeObjekts einer String-Variablen überein. Strukturen dürfen wie Klassen mit eigenen New-Konstrukturen ausgestattet werden. Im Gegensatz zu Klassen müssen diese Konstruktoren aber Parameter aufweisen. Der Defaultkonstruktor New ohne Parameter, der alle Elemente der Struktur mit ihren Defaultwerten initialisiert (also Zeichenketten mit "", Zahlen mit 0 etc.), kann nicht durch einen eigenen parameterlosen Konstruktor überschrieben werden.
•
Strukturen können weder andere Klassen oder Strukturen erben, noch können sie selbst vererbt werden. (Strukturen gelten als NotInheritable.) Strukturen können aber wie Klassen Schnittstellen implementieren.
•
Die Klassenvariablen von Strukturen dürfen keine Instanzen der eigenen Struktur enthalten. (Daher ist es unmöglich, die LinkedList-Klasse aus dem vorherigen Abschnitt als Struktur zu definieren: Dort sind die beiden Klassenvariablen nextItem und previousItem selbst vom Typ LinkedList deklariert und verweisen auf andere LinkedList-Objekte. Genau das ist bei Strukturen nicht möglich.) VERWEIS
•
Weitere Details finden Sie in der Hilfe, wenn Sie nach Strukturen und Klassen suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/vbcn7/html/vaconStructuresAndClasses.htm
vector3d-Beispiel Im folgenden Beispiel wird die Struktur vector3d zur Verwaltung dreidimensionaler Vektoren definiert. Jeder Vektor besteht aus den drei Double-Elementen x, y und y, die öffentlich zugänglich sind. Darüber hinaus ist die Struktur mit einer Reihe von Methoden und Eigenschaften ausgestattet. Der Konstruktur New erleichtert die Initialisierung neuer vector3d-Variablen. Als Parameter müssen die Werte für x, y und y übergeben werden. Die Methode ToString zeigt den Inhalt der Zeichenkette in der Form [1; 2; 3] an. Die Read-Only-Methode Length errechnet die Länge des Vektors (indem die Wurzel aus der Summe der Quadrate der Einzelkomponenten ermittelt wird). Scale multipliziert eine vector3d-Struktur mit einem Faktor. Beachten Sie, dass es grundsätzlich zwei Möglichkeiten gibt, Methoden wie Scale zu deklarieren. Die eine besteht darin,
wie im hier vorliegenden Beispiel die Strukturvariable direkt zu verändern. Der Aufruf erfolgt in der Form vec.Scale(2). Die andere Variante besteht darin, dass Scale als eine Funktion deklariert wird, die die Strukturvariable unverändert lässt und stattdessen das Ergebnis der Operation zurückgibt. Die Anwendung der Methode würde dann so aussehen: v1 = v1.Scale(2) oder v2 = v1.Scale(3). Ein Vorteil der zweiten Variante besteht darin, dass auch
244
7 Objektorientierte Programmierung
Verkettungen der Form v2 = v1.Add(2).Scale(4) möglich sind. Wie so oft ist es aber eine Geschmacksfrage, welche Vorgehensweise intuitiver erscheint. Sie sollten die einmal gewählte Variante aber konsequent einsetzen! Für die Methode Add liegen zwei alternative Deklaration vor. Die Variante mit einem Parameter verändert die als Parameter übergebene Struktur: v1.Add(v2) entspricht also v1 = v1 + v2. Die Variante mit zwei Parametern ist als Shared deklariert und kann losgelöst von einer Struktur zur Addition zweier Vektoren verwendet werden: v1 = vector3d.Add(v2, v3). Innerhalb der Add-Prozedur wird dazu eine temporäre Variable verwendet, deren Inhalt als Ergebnis zurückgegeben wird. ' Beispiel oo-programmierung\structure-test Public Structure vector3d Public x As Double Public y As Double Public z As Double Public Sub New(ByVal x As Double, ByVal y As Double, _ ByVal z As Double) Me.x = x Me.y = y Me.z = z End Sub Public Overrides Function ToString() As String Return "[" + x.ToString + "; " + y.ToString + "; " + _ z.ToString + "]" End Function Public ReadOnly Property Length() As Double Get Return Math.Sqrt(x ^ 2 + y ^ 2 + z ^ 2) End Get End Property Public Sub Scale(ByVal factor As Double) x *= factor y *= factor z *= factor End Sub Public Sub Add(ByVal sum As vector3d) x += sum.x y += sum.y z += sum.z End Sub
7.4 Vererbung
245
Public Shared Function Add(ByVal sum1 As vector3d, _ ByVal sum2 As vector3d) As vector3d Dim tmp As vector3d = sum1 tmp.Add(sum2) Return tmp End Function End Structure
Anwendung der vector3d-Struktur Die Anwendung der vector3d-Struktur ist ausgesprochen intuitiv, wie die folgenden Zeilen beweisen. ' Beispiel oo-programmierung\structure-test Sub Main() Dim v1 As New vector3d(1, 2, 3) Dim v2, v3 As vector3d v1.Add(New vector3d(1, 0, 0)) v2 = v1 v2.Scale(2.5) v3 = vector3d.Add(v1, v2) Console.WriteLine("v1 = {0}", v1) Console.WriteLine("v2 = {0}", v2) Console.WriteLine("v3 = {0}", v3) Console.WriteLine("v3.Length = {0}", v3.Length) End Sub
Im Konsolenfenster wird das folgende Ergebnis angezeigt: v1 = [2; 2; v2 = [5; 5; v3 = [7; 7; v3.Length =
7.4
3] 7,5] 10,5] 14,4308696896618
Vererbung
Vererbung bedeutet, dass eine Klasse die Klassenmitglieder (Variablen, Methoden, Prozeduren) einer anderen Klasse übernimmt. Vererbung wird dazu verwendet, um redundanten Code zu vermeiden. Beispielsweise können Sie zwei Spezialklassen von einer Basisklasse ableiten. Alle gemeinsamen Merkmale werden nur einmal in der Basisklasse definiert. Im Code der Spezialklassen brauchen Sie sich dann nur noch um deren Besonderheiten zu kümmern. Was hier so einfach klingt, ist in der Realität meist doch mit erheblichem Aufwand verbunden. Wenn eine Klasse nicht von Anfang an unter dem Gesichtspunkt einer späteren Er-
246
7 Objektorientierte Programmierung
weiterung (Vererbung) entwickelt wurde, kann jede Änderung größere Probleme mit sich bringen (die oft Mängel im Design der Ausgangsklasse sichtbar machen).
7.4.1
Syntax
Im Klassencode wird Vererbung durch die Inherits-Anweisung durchgeführt. Inherits gibt an, welche Klasse die neue Klasse erbt. Bei der Basisklasse kann es sich sowohl um eine selbst definierte Klasse als auch um eine der zahllosen Klassen der .NET-Bibliotheken handeln. (Sie können also auch bereits vorhandene Klassen erweitern.) Inherits muss am Beginn einer Klassendefinition angegeben werden. ' Beispiel oo-programmierung\vererbung-intro Class class1 Public x As Integer Public y As Integer Public Sub m1() y = 2 * x End Sub End Class Class class2 Inherits class1 Public z As Integer Public Sub m2() z = x + y End Sub End Class
Verwendung vererbter Klassen Die Verwednung vererbter Klassen unterscheidet sich nicht von der gewöhnlicher Klassen. Wenn Sie ein Objekt der Klasse class2 erzeugen, dann stehen Ihnen anschließend alle öffentlichen Klassenvariablen, Methoden und Eigenschaften zur Verfügung, die in class1 und class2 gemeinsam deklariert sind (also obj.m1(), obj.m2(), obj.x, obj.y und obj.z). Objekte abgeleiteter Klassen dürfen auch in Variablen gespeichert werden, die für die Basisklasse deklariert sind (aber nicht umgekehrt). Im folgenden Beispiel werden die Variablen obj1a und obj2a jeweils mit einem neuen Objekt der Klassen class1 und class2 initialisiert. Anschließend wird ein Verweis auf obj2a in obj1b gespeichert. Dieser Verweis wird in der nächsten Zuweisung in obj2b gespeichert. Dabei ist eine Objektumwandlung durch CType erforderlich, weil der Compiler annimmt, dass obj1b ein Objekt des Typs class1 enthält. Wie die folgenden Konsolenausgaben beweisen, verweisen sowohl obj1b als auch obj2b auf ein class2-Objekt. Das Beispiel zeigt einmal mehr, dass jedes Objekt weiß, welcher Klasse es angehört – vollkommen unabhängig davon, wie die Variable deklariert ist.
7.4 Vererbung
247
Dim obj1a, obj1b As class1 Dim obj2a, obj2b As class2 obj1a = New class1() obj2a = New class2() obj1b = obj2a obj2b = CType(obj1b, class2) Console.WriteLine("TypeName(obj1b)=" + TypeName(obj1b)) Console.WriteLine("TypeName(obj2b)=" + TypeName(obj2b))
Die Ausgabe des Programms sieht so aus: TypeName(obj1b)=class2 TypeName(obj2b)=class2
Beachten Sie, dass es umgekehrt unmöglich ist, ein class1-Objekt in einer class2-Variable zu speichern. Die Zuweisung obj2b = obj1a wird bereits vom Compiler als ungültig zurückgewiesen. Wenn Sie es mit obj2b = CType(obj1a, class2) versuchen, akzeptiert zwar der Compiler den Code, aber nun tritt der Fehler bei der Ausführung aus (InvalidCastException).
Mehrfachvererbung Das Konzept der Vererbung kann verschachtelt werden: Sie können also zuerst eine Klasse A deklarierien, dann B von A ableiten und dann C von B. C enthält schließlich alle Klassenmitglieder von A und B.
VERWEIS
Von dieser Verschachtelung abgesehen gibt es aber keine Möglichkeit, dass eine Klasse von mehreren anderen Klassen gleichzeitig erbt. Innerhalb einer Klassendefinition ist daher nur eine einzige Inherits-Anweisung zulässig. Statt auf Vererbung können Sie in manchen Fällen auch auf Schnittstellen zurückgreifen. Schnittstellen bieten zwar nicht den Komfort der Vererbung, geben aber die Möglichkeit, in einer Klasse die Merkmale mehrerer unterschiedlicher Schnittstellen zu realisieren.
Defaultvererbung Alle Klassen sind per Default von Object abgeleitet, alle Strukturen von ValueType. Das kann weder bei Klassen noch bei Strukturen geändert werden: Bei Klassen können Sie zwar Inherits überklasse angeben, aber auch diese Überklasse ist zwangsläufig selbst wieder von Object abgeleitet; und bei Strukturen kann Inherits gar nicht verwendet werden.
MustInherit und NotInheritable Am Beginn der Klassendefinition (also vor Class name ...) können die Schlüsselwörter MustInherit oder NotInheritable vorangestellt werden:
248
•
7 Objektorientierte Programmierung
MustInherit bedeutet, dass die Klasse in dieser Form nicht unmittelbar verwendet
werden kann, d.h., dass es nicht möglich ist, ein Objekt dieser Klasse zu erzeugen. Die Klasse kann ausschließlich als Basisklasse bei der Definition anderer Klassen verwendet werden. •
NotInheritable bedeutet, dass die Klasse gewöhnlich angewendet werden kann, dass es aber nicht möglich ist, neue Klassen davon abzuleiten (zu vererben). NotInheritable verhindert also eine weitere Vererbung.
7.4.2
Basisklasse erweitern und ändern
Das Ziel von Vererbung besteht immer darin, eine vorhandene Basisklasse in irgendeiner Form zu erweitern bzw. zu verändern. Eine Erweiterung um zusätzliche Klassenvariablen, Methoden oder Eigenschaften ist nicht weiter schwierig: Wie das Einführungsbeispiel am Beginn dieses Abschnitts gezeigt hat, fügen Sie dazu einfach den Code für die neuen Schlüsselwörter in die Klasse ein. Bei einem Objekt der Klasse class2 stehen damit die Klassenvariablen x, y und z sowie die Methoden m1 und m2 zur Verfügung.
Overloads, Overrides und Shadows Deutlich komplexer ist die Veränderung bereits vorhandener Schlüsselwörter: Hier bedarf es klarer Regeln, ob und in welchem Ausmaß eine bereits vorhandenes Schlüsselwort durch eine neues, gleichnamiges Schlüsselwort ersetzt werden kann. VB.NET kennt drei unterschiedliche Mechanismen, um vorhandene Schlüsselwörter zu ersetzen. Diese Mechanismen sehen auf ersten Blick sehr ähnlich aus, unterscheiden sich aber in der internen Handhabung durch den Compiler. •
Overrides: Overrides ersetzt eine Methode oder Eigenschaft der Basisklasse. Die neue Methode oder Eigenschaft muss sowohl dieselben Parameter als auch dieselbe Gültigkeitsebene (z.B. Private, Public) aufweisen. Overrides kann nur verwendet werden, wenn das Schlüsselwort in der Basisklasse als Overridable oder MustOverride deklariert ist. Per Default gelten eigene Methoden und Eigenschaften als NotOverridable. Jede mit Overrides deklarierte Methode oder Eigenschaft gilt automatisch selbst wieder als Overridable, ohne dass dies explizit angegeben
wird. MustOverride-Deklarationen dienen nur zur Angabe der Parameterliste. Es darf aber weder ein Code für die Prozedur angegeben werden, noch darf die Deklaration mit End Sub/Function/Property abgeschlossen werden.
•
Overloads: Auch Overloads ersetzt eine Methode oder Eigenschaft der Basisklasse. Dabei kommt derselbe Überladungsmechanismus zum Einsatz, der es auch erlaubt, mehrere gleichnamige Prozeduren innerhalb eines Moduls oder einer Klasse zu definieren (siehe Abschnitt 5.3.4).
Die Überladung gilt allerdings nur, solange der Compiler dem Objekt die richtige Klasse zuordnet. Wenn Sie die Methode m1 überladen, dann wird durch obj.m1() die
7.4 Vererbung
249
neue Methode ausgeführt, aber durch CType(obj, basisklasse).m1() die Methode der Basisklasse! Overloads kann nicht verwendet werden, wenn sich bei einer Methode oder Eigenschaft
nur der Typ des Rückgabewerts (aber nicht die Parameterliste) verändert. •
Shadows: Shadows verdeckt alle gleichnamigen Schlüsselwörter der Basisklasse. Dazu ist nicht erforderlich, dass das Basisschlüsselwort und das neue Schlüsselwort denselben Typ, dieselbe Gültigkeitsebene oder dieselben Parameter aufweist. Shadows kann auch bei der Deklaration von Variablen oder Konstanten verwendet werden.
Mit Shadows können Sie beispielsweise eine Klassenvariable der Basisklasse A durch eine Eigenschaft der abgeleiteten Klasse B ersetzen. Wenn in B auch nur eine einzige Methode m1 mit Shadows deklariert ist, dann verdeckt diese Methode alle in A deklarierten Methoden m1. Wie Overloads gilt auch Shadows nur solange, wie der Compiler dem Objekt die richtige Klasse zuordnet. Jetzt stellt sich natürlich die Frage, welche dieser drei Varianten die Beste ist. Wenn Sie eine eigene Klassenbibliothek erstellen und somit Einfluss auf die Basisklassen haben, ist die Kombination aus Overridable (Basisklasse) und Overrides (abgeleitete Klasse) die im Sinne der objektorientierten Programmierung beste Lösung. Overrides bietet als einzige der drei Varianten die Sicherheit, dass die ursprüngliche Eigenschaft oder Methode auf keinen Fall mehr zugänglich ist (auch nicht, wenn ein Objekt durch CType in ein Objekt der Basisklasse umgewandelt wird). Overloads bietet sich dann an, wenn Sie eine fremde Klasse vererben und eine einzelne Eigenschaften oder Methoden verändern möchten, die in der Basisklasse als NotOveridable gilt. Shadows sollte nur zum Einsatz kommen, wenn Sie den Typ oder die Gültigkeitsebene
eines Schlüsselworts bzw. den Typ eines Rückgabewerts ändern möchten. Einen Sonderfall stellen New-Konstruktoren dar: Diese werden in der neuen Klasse prinzipiell ohne zusätzliche Kennzeichner deklariert. Es ist weder notwendig noch zulässig, einen New-Konstruktor mit Overridable zu kennzeichnen.
Zugriff auf Eigenschaften und Methoden der Basisklasse Innerhalb des Klassencodes können Sie mit den drei Schlüsselwörter Me, MyBase und MyClasse auf die aktuelle Objektinstanz verweisen. Die drei Schlüsselwörter unterscheiden sich dadurch, welche Eigenschaften oder Methoden damit aufgerufen werden. •
Me: Dieses aus früheren Beispielen schon vertraute Schlüsselwort verweist auf die Instanz der Klasse, die gerade bearbeitet wird. (Wenn Sie die Methode obj.m1() aufrufen, dann verweist Me innerhalb des Codes der Klasse m1 auf obj.) Das Schlüsselwort kann verwendet werden, wenn eine Eigenschaft oder Methode eine Instanz der Klasse zurückgeben soll. Me.methode ruft ebenso wie einfach methode die zur Objektinstanz passende Methode auf. (Die Anwendung von Me ist daher in den meisten Fällen optional.)
250
•
7 Objektorientierte Programmierung
MyBase: MyBase verweist wie Me auf die aktuelle Objektinstanz. Durch MyBase.schlüsselwort können Sie aber Schlüsselwörter der abgeleiteten Basisklasse nutzen. Insbesondere können Sie durch MyBase.New den Konstruktor und durch MyBase.Finalize den Destruk-
tor der Basisklasse aufrufen. MyBase.methode ruft eine Methode der Basisklasse auf, selbst dann, wenn es in der aktu-
ellen Klasse eine gleichnamige Neudefinition gibt. •
MyClass: Auch MyClass verweist auf die aktuelle Objektinstanz. Anders als bei Me werden durch MyClass in jedem Fall die unmittelbar in der Klasse definierten Methoden
oder Eigenschaften aufgerufen. MyClass.methode ruft die in der aktuellen Klasse definierte Methode auf, selbst dann, wenn aufgrund des Objekttyps eigentlich eine Overrides-Methode oder -Eigenschaft einer abgeleiteten Klasse aufgerufen werden müsste. (Zu MyClass gibt es in der Online-
VERWEIS
Hilfe ein gut verständliches Beispiel.) Grundsätzlich können Sie in der abgeleiteten Klasse nur auf solche Klassenvariablen, Eigenschaften und Methoden der Basisklasse zugreifen, die dort als Public oder Protected deklariert sind, nicht aber auf solche, die mit Private deklariert sind. Protected bedeutet, dass die Schlüsselwörter zwar von außen unzugänglich sind,
dass sie aber in vererbten Klassen verwendet werden dürfen. Einen ausführlichen Überblick über die Gültigkeitsebenen von Schlüsselwörtern gibt Abschnitt 7.9. Dort werden die vier Schlüsselwörter Private, Protected, Friend und Public miteinander verglichen.
Beispiel Das folgende Beispiel erfüllt keine konkrete Aufgabe, sondern demonstriert die verschiedenen Mechanismen, um vorgegebene Methoden in einer abgeleiteten Klasse zu verändern. (Dieselben Mechanismen können auch für Eigenschaften angewendet werden.) Ausgangspunkt des Beispiels ist die Klasse a mit den Methoden m1, m2 und m3. Jede dieser Methoden steht in zwei Varianten zur Verfügung, einmal ohne Parameter und einmal mit einem Integer-Parameter. Die Klasse b erbt a. m1 wird mittels Shadows durch eine neue Version ersetzt. Das neue m1 ersetzt sowohl m1() als auch m1(Integer)! Das bedeutet, dass die Methode m1(Integer) für Objekte der Klasse b nicht mehr zugänglich ist. (Es bestünde aber natürlich die Möglichkeit, in der Klasse b auch m1(Integer) neu zu implementieren.) m2 wird mittels Overloads durch eine neue Version ersetzt. m2(Integer) wird dadurch nicht
berührt und bleibt weiter zugänglich. m3 wird schließlich mittels Overrides durch eine neue Version ersetzt. Bei der Deklaration muss auch Overloads angegeben werden, weil in a mehrere gleichnamige Varianten von m3 definiert sind. m3(Integer) bleibt weiter zugänglich.
7.4 Vererbung
251
' Beispiel oo-programmierung\overloads_overrides_shadows Class a Public Sub m1() Console.WriteLine("a.m1") End Sub Public Sub m1(ByVal x As Integer) Console.WriteLine("a.m1(x)") End Sub Public Sub m2() Console.WriteLine("a.m2") End Sub Public Sub m2(ByVal x As Integer) Console.WriteLine("a.m2(x)") End Sub Public Overridable Sub m3() Console.WriteLine("a.m3") End Sub Public Overridable Sub m3(ByVal x As Integer) Console.WriteLine("a.m3(x)") End Sub End Class Class b Inherits a Public Shadows Sub m1() Console.WriteLine("b.m1") End Sub Public Overloads Sub m2() Console.WriteLine("b.m2") End Sub Public Overloads Overrides Sub m3() Console.WriteLine("b.m3") End Sub End Class
In Main wird ein neues Objekt der Klasse b erzeugt. Der Aufruf der Methoden m1 bis m3 ohne Parameter führt erwartungsgemäß zu den Ausgaben b.m1, b.m2 und b.m3. Interessanter ist das Ergebnis, wenn die Variable mit CType in ein Objekt des Typs a umgewandelt wird: Bei Shadows und Overloads wird nun die alte Version von m1 bzw. m2 aufgerufen. Einzig die Wirkung von Overrides ist so nachhaltig, dass trotz der Objektumwandlung weiterhin die neue Methode aufgerufen wird.
252
7 Objektorientierte Programmierung
Module Module1 Sub Main() Dim obj As New b() obj.m1() ' obj.m1(123) ist nicht zugänglich obj.m2() ' obj.m2(123) ist zugänglich obj.m3() ' obj.m3(123) ist zugänglich CType(obj, a).m1() CType(obj, a).m2() CType(obj, a).m3() End Sub End Module
Das Ergebnis des Programms sieht so aus: b.m1 b.m2 b.m3 a.m1 a.m2 b.m3
7.4.3
LinkedList-Beispiel
Die Ausgangsidee für dieses Beispiel ist eigentlich recht einfach: Aus der in diesem Kapitel vorgestellten LinkedList-Klasse soll eine neue LinkedListTime-Klasse abgeleitet werden. Die Neuerung dieser Klasse besteht darin, dass in zwei privaten Klassenvariablen jeweils das Datum und die Uhrzeit der letzten Änderung der Daten bzw. des letzten Lesezugriffs gespeichert werden. Diese Daten können mit den beiden neuen Methoden GetLastWrite bzw. GetLastAccess gelesen werden.
VERWEIS
Auf den ersten Blick klingt die Aufgabenstellung so, als ließe sich die neue Klasse LinkedListTime in ein paar Minuten programmieren. Tatsächlich betrug der Aufwand mehrere Stunden und erforderte ca. 120 Codezeilen für die neue Klasse (gegenüber 190 Zeilen für die Basisklasse, die in einigen Details verändert werden musste, um eine Erweiterung überhaupt möglich zu machen). Weitere Vererbungsbeispiele finden Sie in den Abschnitten 14.12.1 (Steuerelemente vererben) und 15.2.5 (Fenster bzw. Formulare vererben).
7.4 Vererbung
253
LinkedList-Änderungen Die meisten Änderungen in der Ausgangsklasse sind kosmetischer Natur: Die Daten des Objekts werden nun in den privaten Klassenvariablen _nextItem, _previousItem und _Value gespeichert. Der Zugriff auf diese Variablen erfolgt durch die Eigenschaften nextItem, previousItem und Value. Außerdem wurden bei einigen Eigenschaften und Methoden die Deklarationen um das Schlüsselwort Overrideable ergänzt. In den folgenden Zeilen sind aus Platzgründen nur die Deklarationen aller Schlüsselwörter der Klasse abgedruckt. ' Beispiel oo-programmierung\linkedlist4, Datei linkedlist.vb Class LinkedList ' Klassenvariablen Private _nextItem As LinkedList Private _previousItem As LinkedList Private _value As String ' Methoden Public Sub New(ByVal s As String) Public Function AddAfter(ByVal s As String) As LinkedList Public Function AddBefore(ByVal s As String) As LinkedList Public Sub Remove() Public Function GetNext() As LinkedList Public Function GetPrevious() As LinkedList Public Overrides Function ToString() As String Public Function ItemsText(Optional ByVal max As Integer = 10, _ Optional ByVal delimitor As String = " ") As String ' Eigenschaften Protected Property previousItem() As LinkedList Protected Property nextItem() As LinkedList Public Overridable Property Value() As String Public ReadOnly Property Count() As Integer Public ReadOnly Property [Next]() As LinkedList Public ReadOnly Property Previous() As LinkedList Default Public Property Item(ByVal n As Integer) As LinkedList End Class
LinkedListTime-Klasse Der Code der LinkedListTime-Klasse beginnt mit der Inherits-Anweisung und der Deklaration der beiden neuen Klassenvariablen _lastAccess und _lastWrite.
254
7 Objektorientierte Programmierung
' Beispiel oo-programmierung\linkedlist4, Datei linkedlisttime.vb Class LinkedListTime Inherits LinkedList ' zwei neue Klassenvariablen Private _lastAccess As Date Private _lastWrite As Date ... weitere Methoden und Eigenschaften End Class
Neue oder geänderte Methoden für LinkedListTime Der neue Konstruktor New greift auf den New-Konstruktor der Basisklasse zurück. Außerdem wird der Eigenschaft lastWrite die aktuelle Uhrzeit zugewiesen. (Der Code zu lastWrite folgt etwas weiter unten. Kurz gefasst wird die Uhrzeit sowohl in _lastAccess als auch in _lastWrite gespeichert.) Public Sub New(ByVal s As String) MyBase.New(s) lastWrite = Now End Sub
Die beiden Methoden GetLastAccess und GetLastWrite geben öffentlichen Lesezugriff auf _lastAccess und _lastWrite. Public Function GetLastAccess() As Date Return _lastAccess End Function Public Function GetLastWrite() As Date
... wie GetLastAccess
Die von LinkedList vertrauten Methoden AddAfter/-Before und GetNext/-Previous mussten für LinkedListTime wegen des geänderten Rückgabetyps neu implementiert werden. Aus eben diesem Grund muss bei der Deklaration Shadows verwendet werden. (Overrides oder Overloads können hier nicht eingesetzt werden.) Public Shadows Function AddAfter(ByVal s As String) As LinkedListTime Dim newitem As New LinkedListTime(s) newitem.previousItem = Me newitem.nextItem = Me.nextItem Me.nextItem = newitem Return newitem End Function Public Shadows Function AddBefore(...) ... wie AddAfter Public Shadows Function GetNext() As LinkedListTime Return nextItem End Function Public Shadows Function GetPrevious() ... wie GetNext
7.4 Vererbung
255
Die Methoden Remove, ToString und ItemsText der Basisklasse brauchen nicht verändert zu werden. Sie funktionieren unverändert auch für LinkedListTime-Objekte.
Neue oder geänderte Eigenschaften für LinkedListTime Die Eigenschaften lastWrite und lastAccess vereinfachen den klasseninternen Zugriff auf _lastWrite und -Access. Bemerkenswert ist vor allem der Set-Teil von lastWrite, der auch _lastAccess verändert. Protected Property lastWrite() As Date Get Return _lastWrite End Get Set(ByVal Value As Date) _lastWrite = Value _lastAccess = Value End Set End Property Protected Property lastAccess() As Date ... wie lastWrite
Die neue Version der Value-Eigenschaft unterscheidet sich von der Basisvariante dadurch, dass bei jedem Zugriff auch die Eigenschaft lastWrite bzw. lastAccess geändert wird. Public Overrides Property Value() As String Get lastAccess = Now Return MyBase.Value End Get Set(ByVal Value As String) MyBase.Value = Value lastWrite = Now End Set End Property previous und nextItem sind nur für die klasseninterne Verwaltung gedacht. Ihre Aufgabe besteht darin, das vorausgehende bzw. nachfolgende LinkedListTime-Objekt zu ermitteln. Da MyBase.previousItem als Rückgabewert ein LinkedList-Objekt liefert, muss dieses in ein LinkedListTime-Objekt umgewandelt werden. (Das funktioniert natürlich nur dann, wenn MyBase.previousItem tatsächlich ein LinkedListTime-Objekt ist! Solange ein LinkedListTime-
Objekt nur mit den hier vorgestellten Methoden bearbeitet wird, ist das sichergestellt.) Protected Shadows Property previousItem() As LinkedListTime Get Return CType(MyBase.previousItem, LinkedListTime) End Get Set(ByVal Value As LinkedListTime) MyBase.previousItem = Value End Set
256
7 Objektorientierte Programmierung
End Property Protected Shadows Property nextItem() ... wie previousItem
Die öffentlich zugänglichen Eigenschaften Next und Previous greifen auf die gerade vorgestellten Eigenschaften zurück. Public Shadows ReadOnly Property [Next]() As LinkedListTime Get Return nextItem End Get End Property Public Shadows ReadOnly Property Previous() ... wie Next Item kann auf MyBase.Item zurückgreifen, muss aber im Get-Teil mit CType eine Typum-
wandlung durchführen. Default Public Shadows Property Item(ByVal n As Integer) _ As LinkedListTime Get Return CType(MyBase.Item(n), LinkedListTime) End Get Set(ByVal Value As LinkedListTime) MyBase.Item(n) = Value End Set End Property
Als einzige Eigenschaft kann Count unverändert genutzt werden.
7.5
Schnittstellen (interfaces)
VERWEIS
Schnittstellen (englisch interfaces) dienen dazu, gemeinsame Merkmale unterschiedlicher Klassen oder Strukturen zu definieren. Eine Schnittstelle besteht lediglich aus einer simplen Aufzählung von Schablonen (also Deklarationen ohne Code) für Methoden, Eigenschaften und Ereignisse. Klassen, die eine Schnittstelle implementieren, müssen für alle Schablonen realen Code angeben. Der Sinn von Schnittstellen besteht darin, dass Objekte unterschiedlicher Klassen, die alle eine bestimmte Schnittstelle realisieren, einheitlich behandelt werden können. Eine sehr anschauliche Demonstration der Anwendungsmöglichkeiten von Schnittstellen geben die Klassen des System.Collections-Namensraums, die in Kapitel 9 beschrieben werden.
7.5 Schnittstellen (interfaces)
7.5.1
257
Syntax und Anwendung
Am besten ist das Konzept und die Syntax von Schnittstellen anhand eines einfachen Beispiels zu verstehen: In den folgenden Zeilen wird die Schnittstelle ILen definiert.
Definition einer Schnittstelle Schnittstellen werden mit Interface name eingeleitet und enden mit End Interface. Die Namen von Schnittstellen beginnen überlicherweise mit dem Buchstaben I. Das einzige Merkmal dieser Schnittstelle besteht darin, dass jede Klasse oder Struktur die Methode Len zur Verfügung stellen muss. Diese Methode muss als Ergebnis eine DoubleZahl liefern, die Rückschluss auf die Länge oder Größe der Daten gibt. Innerhalb der Schnittstelle wird daher einfach eine Schablone für die Methode Len angegeben, und zwar ohne die Angabe der Gültigkeitsebene (also ohne Private, Public etc.), ohne konkreten Code für die Realisierung der Methode und ohne End Sub/Function/Property. ' Beispiel oo-programmierung\interfaces-intro Interface ILen Function Len() As Double End Interface
Schnittstellen können andere Schnittstellen erben. Sie können also am Beginn einer Schnittstelle Inherits Ixy angeben, sofern Ixy eine andere Schnittstelle ist. Die resultierende neue Schnittstelle enthält dann auch alle Merkmale der abgeleiteten Schnittstelle. (Anders als bei Klassen ist es sogar möglich, eine Schnittstelle von mehreren anderen Schnittstellen abzuleiten.)
Implementierung (Realisierung) einer Schnittstelle Die beiden Strukturen vector2d und vector3d realisieren die ILen-Schnittstelle. Dazu muss die Schnittstelle am Beginn der Struktur mit Implements angegeben werden. (Mehrere Schnittstellen können entsprechend durch mehrere Implements-Anweisungen genannt werden. Bei Klassen müssen die Implements-Anweisungen unmittelbar einer eventuellen Inherits-Anweisung folgen.) Der Deklaration der Methode Len muss Implements ILen.Len nachgestellt werden, um den Zweck dieser Methode zu verdeutlichen. Der VB.NET-Compiler sollte eigentlich wie der C#-Compiler auch ohne den Implements-Zusatz in der Lage sein, den Zusammenhang zur Schnittstelle herzustellen. Die VB.NET-Syntax schreibt den eigentlich unsinnigen Implements-Nachsatz aber vor. Das macht es möglich, der Methode einen anderen Namen zu geben als den, der durch die Schnittstelle vorgegeben ist – also beispielsweise Function xy() As Double Implements ILen.Len. In der Praxis stiftet das aber meist nur Verwirrung. Die einzig sinnvolle Anwendung besteht darin, in einer Klasse gleichnamige Methoden unterschiedlicher Schnittstellen zu realisieren.
258
7 Objektorientierte Programmierung
Structure vector2d Implements ILen Public x, y As Double Public Function Len() As Double Implements ILen.Len Return Math.Sqrt(x ^ 2 + y ^ 2) End Function End Structure Structure vector3d Implements ILen Public x, y, z As Double
HINWEIS
Public Function Len() As Double Implements ILen.Len Return Math.Sqrt(x ^ 2 + y ^ 2 + z ^ 2) End Function End Structure
Wenn Sie eine Schnittstelle implementieren, müssen Sie alle Eigenschaften und Methoden dieser Schnittstelle implementieren. In manchen Fällen kann es sein, dass das nicht sinnvoll ist; dann müssen Sie in Ihre eigene Klasse zumindestens die Prozedurdefinition für die Eigenschaft oder Methode angeben, ohne diese Definition mit Code zu füllen. Eventuell sollten Sie beim Aufruf der Eigenschaft oder Methode eine NotImplementedException auslösen, um dem Anwender der Klasse klar zu machen, dass diese Eigenschaft bzw. Methode nicht zur Verfügung steht.
Objekte einer gemeinsamen Schnittstelle verarbeiten Die folgenden Zeilen zeigen die Deklaration und Initialisierung je eines Objekts der Klassen vector2d und -3d. Beide Objekte werden dann an die Prozedur WriteLen übergeben, die die Länge des Vektors in einem Konsolenfenster anzeigt. Die Besonderheit von WriteLen besteht darin, dass dessen Parameter x mit dem Typ ILen deklariert ist. Damit kann ein Objekt einer beliebigen Klasse übergeben werden, vorausgesetzt die Klasse unterstützt die ILen-Schnittstelle. (Innerhalb der Prozedur können nur die durch ILen deklarierten Eigenschaften und Methoden genutzt werden. ILen beschreibt also den gemeinsamen Nenner aller Klassen mit ILen-Schnittstelle.) ' Beispiel oo-programmierung\interfaces-intro Sub Main() Dim v2 As vector2d Dim v3 As vector3d v2.x = 3 : v2.y = 2 v3.x = 4 : v3.y = 5 : v3.z = 6 WriteLen(v2) WriteLen(v3) End Sub
7.5 Schnittstellen (interfaces)
259
Sub WriteLen(ByVal x As ILen) Console.WriteLine(x.Len) End Sub
Schnittstellen versus Vererbung Die Implementierung einer Schnittstelle und die Vererbung einer Klasse sieht auf den ersten Blick sehr ähnlich aus. Tatsächlich gibt es aber fundamentale Unterschiede: •
Eine Klasse darf nur von einer anderen Basisklasse vererbt werden, kann aber beliebig viele Schnittstellen realisieren.
•
Vererbter Code für Methoden oder Eigenschaften kann meist ohne Veränderung genutzt werden (sofern die neue Klasse für die betreffende Eigenschaft oder Methode nicht neue Funktionen realisieren will). Im Gegensatz dazu müssen die durch eine Schnittstelle vorgegebenen Methoden oder Eigenschaften in jeder Klasse vollkommen neu implementiert werden. Schnittstellen dienen daher nicht zur Wiederverwendung von Code, sondern lediglich dazu, den Kommunikationsmechanismus für mehrere unterschiedliche Klassen zu vereinheitlichen.
•
Schnittstellen können auch von Strukturen realisiert werden, während Vererbung nur für Klassen zur Verfügung steht.
7.5.2
IDisposable-Schnittstelle
Die .NET-Bibliotheken enthalten zahllose Schnittstellen, deren Implementierung in eigenen Klassen sinnvoll sein kann: •
IComparable ermöglicht den Vergleich zweier Objekte (siehe auch Abschnitt 9.3.2). Die Schnittstelle ist auch dann wichtig, wenn Objekten sortiert werden sollen.
•
IClonable ermöglicht es, eine identische Kopie eines Objekts zu erzeugen.
•
IConvertible ermöglicht es, das Objekt in einen anderen Typ umzuwandeln.
•
IFormatable ermöglicht es, das Objekt mit ToString auf unterschiedliche Weise in Text-
form darzustellen. •
IEnumerable ermöglicht es, alle Elemente einer Aufzählung mit For-Each zu durchlaufen (siehe Abschnitt 9.2).
•
ISerializable ermöglicht die Steuerung der (De-)Serialisierung eigener Klassen (siehe Abschnitt 10.9).
Dieses Kapitel beschränkt sich allerdings auf ein Beispiel für eine einzige Schnittstelle, nämlich IDisposable. Diese Schnittstelle hilft dabei, eine Objektinstanz kontrolliert durch die Methode Dispose zu löschen. Die Schnittstelle sollte für eigene Klassen realisiert werden, wenn diese überdurchschnittlich viel Speicher beanspruchen, Dateien oder Daten-
260
7 Objektorientierte Programmierung
VERWEIS
bankverbindungen öffnen oder auf eine andere Weise Systemressourcen binden. In so einem Fall ist es nicht sinnvoll, darauf zu warten, dass die garbage collection irgendwann das Objekt wieder freigibt. Stattdessen wird diese Verantwortung an die Anwenderin der Klasse übertragen, die Dispose ausführen muss, sobald das Objekt nicht mehr benötigt wird. Weitere Informationen zu diesem Thema finden Sie bei der Beschreibung der Object.Finalize-Methode und wenn Sie nach Dispose implementieren suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/cpref/html/frlrfSystemObjectClassFinalizeTopic.htm ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpconimplementingdisposemethod.htm
Zur Implementierung der Disposable-Schnittstelle müssen Sie lediglich die Dispose-Methode zur Verfügung stellen. Dabei sollten Sie aber einige Dinge beachten: •
Der Code sollte so formuliert werden, dass ein mehrfacher Aufruf von Dispose zu keinen Fehlern führt.
•
Am Ende von Dispose sollte GC.SuppressFinalize(Me) ausgeführt werden. Damit erreichen Sie, dass für das bereits gelöschte Objekt auf den automatischen Finalize-Aufruf durch die garbage collection verzichtet wird. (Das erhöht die Effizienz der garbage collection.)
•
Falls die eigene Klasse von einer anderen Klasse abgeleitet ist, die selbst die IDisposableSchnittstelle realisiert, muss in der eigenen Dispose-Methode MyBase.Dispose() ausgeführt werden, um auch alle Ressourcen der Basisklasse freizugeben.
•
Falls die Anwenderin der Klasse den Dispose-Aufruf vergisst, sollte sich die FinalizeMethode um die Aufräumarbeiten kümmern.
•
Falls die Klasse eine Close-Methode kennt, sollte diese einfach Dispose aufrufen. (Damit hat obj.Close() dieselbe Wirkung wie obj.Dispose().)
Aus diesen Anforderungen ergibt sich die folgende Schablone, die Sie zur Erstellung eigener Dispose-Methoden verwenden können. Die eigentliche Aufräumarbeit erfolgt in der Prozedur cleanup, die sowohl von Finalize als auch von Dispose aufgerufen wird. Die Variable clean stellt sicher, dass der Code nur einmal ausgeführt wird. Class Class1 Implements IDisposable Private clean As Boolean = False ' Finalize gibt es nur für den Fall, 'dass auf obj.Dispose() vergessen wird Protected Overrides Sub Finalize() cleanup() MyBase.Finalize() End Sub
7.5 Schnittstellen (interfaces)
261
Overridable Overloads Sub Dispose() Implements IDisposable.Dispose cleanup() ' nur wenn die Basisklasse ebenfalls IDisposable implementiert: ' MyBase.Dispose() GC.SuppressFinalize(Me) End Sub Private Sub cleanup() If clean = True Then Exit Sub 'alles schon erledigt clean = True ... Aufräumarbeiten End Sub End Class
IDisposeable-Beispiel Mit einem Objekt der Klasse AppendTextToFile können Sie eine Textdatei öffnen und an diese Text hinzufügen (Methode WriteLine). Innerhalb der Beispielklasse wird dazu ein IO.StreamWriter-Objekt verwendet (siehe Abschnitt 10.5.3). Objekte der AppendTextToFile-Klasse können durch Close oder Dispose aus dem Speicher entfernt werden. Innerhalb der Klasse wird dazu die Methode cleanup ausgeführt, um das StreamWriter-Objekt zu schließen. Wenn die Anwenderin den Aufruf von Dispose oder Close vergisst, wird durch die garbage collection die Methode Finalize aufgerufen, die dann ebenfalls cleanup aufruft. Die Variable clean stellt sicher, dass auch bei einen irrtümlichen doppelten Aufruf von Close oder Dispose kein Fehler auftritt. ' Beispiel oo-programmierung\idisposable-test Class AppendTextToFile Implements IDisposable Private clean As Boolean = False Private sw As IO.StreamWriter Public Sub New(ByVal filename As String) sw = New IO.StreamWriter(filename, True) End Sub Public Sub WriteLine(ByVal s As String) sw.WriteLine(s) sw.Flush() 'Zeile sofort physikalisch speichern End Sub ' Finalize gibt es nur für den Fall, ' dass obj.Dispose() oder obj.Close() vergessen wird Protected Overrides Sub Finalize() cleanup() MyBase.Finalize() End Sub
262
7 Objektorientierte Programmierung
' Objekt dezidiert schließen Public Sub Close() Dispose() End Sub Overridable Overloads Sub Dispose() Implements IDisposable.Dispose cleanup() GC.SuppressFinalize(Me) End Sub ' Aufräumarbeiten Private Sub cleanup() If clean = True Then Exit Sub 'alles schon erledigt clean = True sw.Close() End Sub End Class
Um die Klasse zu testen, wird in Main zweimal die Prozedur WriteALine aufgerufen. In dieser Prozedur wurde attf.Close auskommentiert. Es müsste daher eigentlich zu einem Fehler kommen, weil die temporäre Datei nach dem ersten Aufruf von WriteALine nicht geschlossen wurde. Dieser Fehler tritt aufgrund der künstlich ausgelösten garbage collection (die zu einem Aufruf von attf.Finalize() führt) nicht auf. Sub Main() WriteALine() GC.Collect() GC.WaitForPendingFinalizers() WriteALine() End Sub
'garbage collection auslösen 'wartet auf das Ende der gc
Sub WriteALine() Dim attf As New AppendTextToFile(IO.Path.GetTempPath() + "test.txt") attf.WriteLine(Now.ToLongTimeString) ' attf.Close() End Sub
7.6
Ereignisse und Delegates
Ereignisse bieten eine Möglichkeit, mit der ein Objekt Informationen über einen geänderten Zustand, geänderte Daten, neue Eingaben, Fehler etc. weitergeben kann. Dazu wird beim Empfänger des Ereignisses eine zuvor vereinbarte Prozedur aufgerufen. Delegates stellen einen Mechanismus zur Verfügung, um Prozeduren, Funktionen oder
Methoden aufzurufen, von denen nur die Adresse bekannt ist. Das ermöglicht es beispielsweise, einen Funktionszeiger an eine Prozedur zu übergeben und die Funktion dort aufzurufen.
7.6 Ereignisse und Delegates
263
Ereignisse und Delegates werden hier (und in den meisten anderen .NET-Büchern) gemeinsam beschrieben, weil Ereignisse intern auf Delegates aufbauen. Ereignisse sind gewissermaßen nur eine Spezialanwendung von Delegates. Ereignisse haben aber den Vorteil, dass Sie sich nicht mit der Komplexität des Delegates-Mechanismus auseinandersetzen müssen.
7.6.1
Ereignisse
Deklaration eines Ereignisses Ereignisse können in Klassen, Modulen, Strukturen und in Schnittstellen mit dem Schlüsselwort Event deklariert werden. Die Deklaration enthält keinen Code und gibt nur den Namen des Ereignisses und die Datentypen der Parameter der Ereignisprozedur an. (Üblicherweise wird als erster Parameter eine Objektinstanz der Klasse übergeben; die weiteren Parameter enthalten ereignisspezifische Daten.) Class Class1 Public Event ValueChanged(ByVal obj As Object, _ ByVal data As Integer) ... End Class
Aufruf eines Ereignisses Innerhalb der Klasse, in der das Ereignis deklariert wurde, kann es durch RaiseEvent ausgelöst werden. RaiseEvent ValueChanged(Me, 123)
Ereignisse empfangen Es gibt zwei Möglichkeiten, ein Ereignis zu empfangen, das durch RaiseEvent ausgelöst wurde: entweder, indem die Objektvariable mit WithEvents deklariert und eine dazu passende Ereignisprozedur mit Handles angegeben wird, oder, indem die Ereignisprozedur mit AddHandler angegeben wird. Bei Ereignissen, die in Strukturen oder Modulen (nicht in Klassen) deklariert sind, bietet AddHandler die einzige Möglichkeit, das Ereignis zu empfangen. Ereignisse, die nicht empfangen werden, verfallen. Wenn also innerhalb einer Klasse RaiseEvent ausgeführt wird, aber keine Prozedur eingerichtet wurde, um das Ereignis zu verarbeiten, kommt es zu keiner Reaktion (auch zu keinem Fehler).
WithEvents und Handles Der einfachste Weg, Ereignisse eines bestimmten Objekt zu empfangen, besteht darin, die Objektvariable auf Modul- oder Klassenebene (also nicht innerhalb einer Prozedur!) mit WithEvents zu deklarieren. Wenn Sie möchten, können Sie das Objekt dabei gleich erzeu-
264
7 Objektorientierte Programmierung
gen (As New Class1(...)) – andernfalls müssen Sie das zu einem späteren Zeitpunkt mit obj1 = New Class1(...) nachholen. Auf jeden Fall können Sie nur dann Ereignisse empfangen, wenn das Objekt auch existiert! Dim WithEvents obj1 As Class1 obj1 = New Class1(...)
Der zweite Schritt besteht nun, die dazugehörende Ereignisprozedur in den Code einzufügen. Dabei hilft Ihnen die Entwicklungsumgebung: Wählen Sie zuerst im linken Listenfeld des Codefensters die Objektvariable aus und dann im rechten Listenfeld das gewünschte Ereignis. Die Entwicklungsumgebung fügt dann eine Prozedurdeklaration nach dem folgenden Schema in den Code ein: Public Sub obj1_ValueChanged(ByVal obj As Object, _ ByVal data As Integer) Handles obj1.ValueChanged ' ... hier müssen Sie Ihren Code einfügen End Sub
Selbstverständlich können Sie die Deklaration für die Ereignisprozedur auch per Tastatur einfügen. Dabei müssen Sie auf die korrekte Parameterliste achten (sie muss der Event-Deklaration entsprechen). Außerdem muss der eigentlichen Deklaration Handles variablenname.ereignisname folgen, damit VB.NET weiß, mit welchem Ereignis die Prozedur verbunden werden soll. (Der Name der Ereignisprozedur – hier als obj1_ValueChanged – ist dagegen irrelevant.)
AddHandler und RemoveHandler Die zweite Variante zum Empfang von Ereignissen besteht darin, die Objektvariable auf gewöhnliche Art (ohne WithEvents) zu deklarieren und dann mit AddHandler die Adresse der Prozedur anzugeben, die das Ereignis verarbeiten soll. Die Adresse ermitteln Sie mit dem Operator AddressOf. Dim obj2 As New Class1() AddHandler obj2.ValueChanged, AddressOf eventproc
Bei der Deklaration der Ereignisprozedur muss wie bei der ersten Variante auf die korrekte Parameterliste geachtet werden. Der Handles-Zusatz ist dagegen nicht erforderlich. Die Entwicklungsumgebung gibt hier keine Hilfe bei der Deklaration dieser Prozedur. Public Sub eventproc(ByVal obj As Object, ByVal data As Integer) ' ... hier müssen Sie Ihren Code einfügen End Sub
Mit RemoveHandler kann eine Ereignisprozedur wieder deaktiviert werden: RemoveHandler obj2.ValueChanged, AddressOf eventproc
Die Vorgehensweise mit AddHandler hat eine Reihe von Vorteilen:
7.6 Ereignisse und Delegates
265
Sie können auf diese Weise auch Ereignisse empfangen, die in Modulen oder Strukturen ausgelöst werden.
•
Der Ort, an dem die Objektvariablen deklariert werden, spielt keine Rolle. (WithEventsVariablen müssen dagegen als Modul- oder Klassenvariablen deklariert werden.)
•
Die Verbindung zwischen dem Ereignis eines Objekts und der Prozedur erfolgt dynamisch (also während der Programmausführung). Das ist vor allem dann praktisch, wenn während des Programms laufend neue Objekte erzeugt werden, deren Ereignisse verarbeitet werden sollen.
•
Eine Ereignisprozedur kann zur Verarbeitung beliebig vieler Ereignisse (unterschiedlicher Objekte) eingesetzt werden, sofern die Parameterliste der Ereignisse identisch ist.
•
Es ist möglich, mit AddHandler für ein Ereignis (eines Objekts) mehrere Ereignisprozeduren anzugeben. Diese werden der Reihe nach aufgerufen.
VERWEIS
•
Besonders häufig mit Ereignissen zu tun haben Sie es bei der Programmierung von Windows-Anwendungen: dort gilt jeder Mausklick, jede Menüauswahl, jede Änderung der Fenstergröße als Ereignis. Es verwundert daher nicht, dass das Verarbeiten von Ereignissen in den Kapiteln zur Windows-Programmierung einen großen Stellenwert hat. Weitere Informationen finden Sie hier: Einführung, Ereignisprozeduren in den Code einfügen: Abschnitt 13.1.2 Ereignisse bei dynamisch erzeugten Steuerelementen: Abschnitt 14.11.2 Ereignisreihenfolge: Abschnitt 15.1.4 Interna der Ereignisverwaltung: Abschnitt 15.2
Beispiel Die Klasse Class1 besteht aus einer privaten Klassenvariable _x, die über die öffentliche Eigenschaft X gelesen und verändert werden kann. Bei jeder Veränderung von X wird das Ereignis ValueChanged ausgelöst. Als Parameter werden ein Verweis auf das Objekt (also Me) sowie der aktuelle Inhalt von _x übergeben. 'Beispiel oo-programmierung\event-intro Class Class1 Private _x As Integer Public Event ValueChanged(ByVal obj As Object, _ ByVal data As Integer) Public Property X() As Integer Get Return _x End Get
266
7 Objektorientierte Programmierung
Set(ByVal Value As Integer) _x = Value RaiseEvent ValueChanged(Me, Value) End Set End Property Public Sub Clear() X = 0 End Sub End Class
In Module1.Main werden die beiden Prozeduren sub1 und sub2 aufgerufen, die die beiden Mechanismen zur Ereignisverwaltung demonstrieren. sub1 greift auf die Modulvariable obj1 zurück, die mit WithEvents deklariert ist. Deren Ereignisse werden durch obj1_ValueChanged verarbeitet. Module Module1 Dim WithEvents obj1 As Class1 Sub Main() sub1() 'Ereignisaufruf mit WithEvents demonstrieren sub2() 'Ereignisempfang mit AddHandler demonstrieren End Sub Sub sub1() obj1 = New Class1() obj1.X = 3 obj1.X = 4 obj1.Clear() End Sub Public Sub obj1_ValueChanged(ByVal obj As Object, _ ByVal data As Integer) Handles obj1.ValueChanged Console.WriteLine( _ "Ereignis class1.ValueChanged (WithEvents), data={0}", data) End Sub ' weitere Prozeduren in Module1 ... End Module
In sub2 wird eine weitere Variable der Klasse Class1 erzeugt. Diesmal wird die Ereignisprozedur ValueChanged_handler durch AddHandler mit dem Ereignis verbunden. Sub sub2() Dim obj2 As New Class1() AddHandler obj2.ValueChanged, AddressOf ValueChanged_handler obj2.X = 1 obj2.X += 2 obj2.Clear() End Sub
7.6 Ereignisse und Delegates
267
Public Sub ValueChanged_handler(ByVal obj As Object, _ ByVal data As Integer) Console.WriteLine( _ "Ereignis class1.ValueChanged (AddHandler), data={0}", data) End Sub
Das Beispielprogramm liefert folgende Ausgabe im Konsolenfenster: Ereignis Ereignis Ereignis Ereignis Ereignis Ereignis
class1.ValueChanged class1.ValueChanged class1.ValueChanged class1.ValueChanged class1.ValueChanged class1.ValueChanged
7.6.2
Delegates
(WithEvents), (WithEvents), (WithEvents), (AddHandler), (AddHandler), (AddHandler),
data=3 data=4 data=0 data=1 data=3 data=0
Ein Delegate ist ein Objekt der Klasse System.Delegate, in dem die Adresse einer Prozedur oder Methode und (optional) der Verweis auf ein Objekt gespeichert werden. Delegates helfen dabei, Funktionen, Unterprogramme oder Methoden aufzurufen, von denen nur die Adresse bekannt ist. Insofern bieten Delegates eine ähnliche Möglichkeit wie die Verwendung von Funktionszeigern (function pointers) in manchen herkömmlichen Programmiersprachen (z.B. C). Da innerhalb des Delegate-Objekts nicht nur die Adresse, sondern auch Kontextinformationen gespeichert werden, ist es ausgeschlossen, dass eine Prozedur mit einer nicht dazu passenden Parameterliste aufgerufen wird. Delegates stellen also einen Sicherheitsmechanismus dar, der Kompatibilitätsprobleme beim Aufruf von Prozeduren von vorneherein vermeidet. Intern basiert jeder Aufruf einer Ereignisprozedur auf Delegates. Der Mechanismus von Delegates ist aber allgemeingültiger als der von Ereignissen und ermöglicht darüber hinausgehende Anwendungen.
Syntax Bevor eine Prozedur durch die Angabe ihrer Adresse aufgerufen werden kann, sind mehrere Schritte erforderlich. Als Erstes wird mit Delegate Sub oder Delegate Function eine Schablone für die aufzurufende Prozedur definiert. Diese Schablone dient lediglich dazu, die Typen der Parameter sowie (bei Funktionen) den Typ des Rückgabewerts zu spezifizieren. Delegate Function mydelegate(ByVal s As String) As String mydelegate kann nun wie eine neue Klasse (wie ein neuer Typ) verwendet werden. Sie können also Variablen diesen Typs deklarieren. Dim func1 As mydelegate
268
7 Objektorientierte Programmierung
In func1 können Sie nun unter Anwendung von AddressOf die Adresse einer beliebigen Funktion speichern, die der Parameterliste von mydelegate entspricht. AddressOf liefert nicht einfach eine Adresse, sondern ein Delegate-Objekt. Wenn Sie AddressOf auf eine Methode (nicht auf eine Prozedur) anwenden, dann enthält das von AddressOf erzeugte Delegate-Objekt auch einen Verweis auf die Objektinstanz, auf die die Methode angewendet werden soll. func1 = AddressOf my_string_function
Die Deklaration von my_string_function muss dem folgenden Muster entsprechen: Function my_string_function(ByVal s As String) As String ... Code der Funktion End Function
Jetzt können Sie die Methode Invoke auf die Variable func anwenden, um die Funktion aufzurufen. An Invoke müssen Sie die bei der Delegate-Definition angegebenen Parameter übergeben. Bei Funktionen liefert Invoke auch den Rückgabewert (ebenfalls im Typ der DelegateDefinition). Dim s As String s = func3.Invoke("abc")
Derselbe Mechanismus funktioniert natürlich nicht nur für Funktionen oder Prozeduren, sondern auch für die Methoden einer Klasse.
Beispiel Im Mittelpunkt dieses Beispiels steht das Unterprogramm WriteStrings. An diese Prozedur wird ein String-Feld sowie eine Funktion zur Verarbeitung von Zeichenketten übergeben. Diese Prozedur ist durch die Delegate-Deklaration string_delegate deklariert. Sie erwartet als Parameter eine Zeichenkette und liefert als Ergebnis wieder eine Zeichenkette zurück. WriteStrings wendet nun diese Funktion auf jedes Element des String-Felds an und schreibt die Resultate in das Konsolenfenster. Mit anderen Worten: An WriteStrings kann eine beliebige, einparametrige Funktion zur Verarbeitung von Zeichenketten übergeben werden. In Main() wird WriteStrings dreimal aufgerufen. Als String-Feld wird jeweils eine Aufzählung von Buchautoren übergeben. Als Funktion zur Bearbeitung der Zeichenketten wird mit Hilfe des Delegate-Mechanismus einmal die Funktion first_word, einmal die Funktion last_word und einmal die VB.NET-Funktion Strings.StrReverse übergeben. ' Beispiel oo-programmierung\delegates-intro Module Module1 Sub Main() Dim s() As String = _ {"Holger Schwichtenberg", "Frank Eller", "Dan Appleman", _ "Brian Bischof", "Gary Cornell", "Jonathan Morrison", _ "Andrew Troelsen"}
7.6 Ereignisse und Delegates
269
Dim func1, func2, func3 As string_delegate func1 = AddressOf first_word func2 = AddressOf last_word func3 = AddressOf Strings.StrReverse WriteStrings(s, func1) WriteStrings(s, func2) WriteStrings(s, func3) End Sub Sub WriteStrings(ByVal s As String(), ByVal func As string_delegate) Dim item, tmp As String For Each item In s tmp = func.Invoke(item) Console.Write("{0} ", tmp) Next Console.WriteLine() End Sub ' Delegate für eine Funktion, die einen String als Parameter ' erwartet und einen String zurückgibt Delegate Function string_delegate(ByVal s As String) As String ' zwei Funktionen zur String-Verarbeitung Function first_word(ByVal s As String) As String Dim pos As Integer s = Trim(s) pos = InStr(s, " ") If pos > 0 Then Return (Left(s, pos - 1)) Else Return s End If End Function Function last_word(ByVal s As String) As String ... ähnlicher Code wie in first_word End Function End Module
Das Programm liefert folgendes Ergebnis im Konsolenfenster: Holger Frank Dan Brian Gary Jonathan Andrew Schwichtenberg Eller Appleman Bischof Cornell Morrison Troelsen grebnethciwhcS regloH rellE knarF namelppA naD fohcsiB nairB llenroC yraG nosirroM nahtanoJ nesleorT werdnA
270
7.6.3
7 Objektorientierte Programmierung
Delegates und Ereignisse kombinieren
Normalerweise merken Sie nichts davon, dass jeder Ereignisaufruf intern auf Delegates beruht, und es braucht Sie auch gar nicht zu kümmern. Sie können aber eine Delegate-Deklaration dazu verwenden, um mehrere Ereignisse, die sich durch dieselbe Parameterliste auszeichen, einheitlich zu deklarieren. Der Vorteil dieser Vorgehensweise besteht darin, dass Ihre Ereignisse mit Sicherheit zueinander kompatibel sind, dass die Deklaration der Ereignisse übersichtlicher wird und Sie redundanten Code sparen. Die Grundidee besteht darin, zuerst eine Delegate-Deklaration durchzuführen, um darin die Parameterliste für das Ereignis anzugeben. Bei der nachfolgenden Deklaration der Ereignisse können Sie dann auf die Delegate-Deklaration zurückgreifen. Public Delegate Sub mydelegate(ByVal obj As Object, _ ByVal data As Integer, ...) Public Event myEvent1 As mydelegate Public Event myEvent2 As mydelegate Public Event myEvent3 As mydelegate ...
Sie können natürlich auch durch die .NET-Klassenbibliothek vordefinierte Delegates verwenden, um Ereignisse zu deklarieren, die mit vertrauten .NET-Ereignissen kompatibel sind. Die folgende Zeile definiert ein Ereignis, das mit den MouseEvent-Ereignissen aus der Windows.Forms-Bibliothek kompatibel ist. Public Event myevent As Windows.Forms.MouseEventHandler
Der Aufruf und die Verarbeitung von derart deklarierten Ereignissen unterscheidet sich nicht von gewöhnlichen Ereignissen. (Es sei hier aber erwähnt, dass die .NET-Bibliothek unterschiedliche Unterklassen zu System.Delegate kennt: MulticastDelegate, AsyncCallback, EventHandler, CrossAppDomainDelegate etc. Damit können verschiedene Sonderformen beim Aufruf von Funktionen und Ereignisprozeduren realisiert werden. In diesem Buch fehlt allerdings der Platz, um auf die Besonderheiten dieser Klassen einzugehen.)
Beispiel Die Klasse Class1 sieht ähnlich aus wie im Event-Beispiel aus Abschnitt 7.6.1. Der Unterschied besteht darin, dass nun je nachdem, ob _x vergrößert oder verkleinert wird, das Ereignis ValueIncreased oder ValueDecreased ausgelöst wird. (Wenn der Wert sich bei einer Zuweisung nicht ändert, kommt es zu gar keinem Ereignis.) Anstatt bei der Deklaration der beiden Ereignisse jedes Mal die Parameterliste anzugeben, wurde die Delegate-Prozedur valuechanged_delegate deklariert und sozusagen als Muster für die Parameterliste angegeben. ' Beispiel oo-programmierung\delegates-events Class Class1 Private _x As Integer Public Delegate Sub valuechanged_delegate(ByVal obj As Object, _ ByVal data As Integer)
7.6 Ereignisse und Delegates
271
Public Event ValueIncreased As valuechanged_delegate Public Event ValueDecreased As valuechanged_delegate Public Property X() As Integer Get Return _x End Get Set(ByVal Value As Integer) Dim oldx As Integer = _x _x = Value If Value > oldx Then RaiseEvent ValueIncreased(Me, Value) ElseIf Value < oldx Then RaiseEvent ValueDecreased(Me, Value) End If End Set End Property Public Sub Clear() X = 0 End Sub End Class
Die folgenden Zeilen zeigen, wie die ValueDecreased- und -Increased-Ereignisse verarbeitet werden können. Module Module1 Dim WithEvents obj1 As Class1 Sub Main() obj1 = New Class1() obj1.X = 3 obj1.X = -1 obj1.Clear() End Sub Public Sub obj1_ValueDecreased(ByVal obj As Object, _ ByVal data As Integer) Handles obj1.ValueDecreased Console.WriteLine( _ "Ereignis class1.ValueDecreased, data={0}", data) End Sub Public Sub obj1_ValueIncreased(ByVal obj As Object, _ ByVal data As Integer) Handles obj1.ValueIncreased Console.WriteLine( _ "Ereignis class1.ValueIncreased, data={0}", data) End Sub End Module
272
7 Objektorientierte Programmierung
Das Programm liefert folgendes Ergebnis im Konsolenfenster: Ereignis class1.ValueIncreased, data=3 Ereignis class1.ValueDecreased, data=-1 Ereignis class1.ValueIncreased, data=0
7.7
Attribute
Attribute bieten die Möglichkeit, zusätzliche Merkmale einer Klasse, eines Parameters, einer Prozedur etc. anzugeben. Dazu gleich ein paar Beispiele: •
Wenn Sie eine Aufzählung (Enum) mit dem Attribut ausstatten, können die Elemente der Aufzählung durch Or kombiniert werden.
•
Wenn Sie eigene Steuerelemente programmieren, können Sie durch diverse -Attribute angeben, welche Eigenschaften im Eigenschaftsfenster angezeigt werden, wie die Eigenschaften gruppiert werden sollen etc.
•
Bei Prozeduren können Sie durch das Attribut eine zeilenweise Ausführung durch den Debugger verhindern.
•
Bei Klassen oder Strukturen erreichen Sie durch das Attribut , dass deren Inhalt serialisiert werden kann (etwa zur Übertragung der Daten in oder aus einer Datei bzw. über eine Netzwerkverbindung).
•
In der Datei AssemblyInfo.vb, die zu jedem VB.NET-Projekt gehört, werden mit Attributen diverse Zusatzinformationen angegebenen, die die Assembly beschreiben (also die resultierenden Programmdatei).
Intern handelt es sich bei Attributen um Klassen, die von System.Attribute abgeleitet sind.
Wozu Attribute? Auf den ersten Blick mag es so erscheinen, als wären Attribute eine Spezialform von Klasseneigenschaften. Das ist aber keineswegs der Fall! Die Anwendungsmöglichkeiten von Attributen liegen ganz woanders: •
Attribute stehen nicht nur für Klassen, sondern für fast alle VB.NET-Sprachkonstrukte zur Verfügung. Daher können Sie mit Attributen nicht nur Zusatzeigenschaften von Klassen einstellen, sondern auch besondere Eigenschaften von Variablen, Eigenschaften, Methoden, Aufzählungen, Assemblies etc.
•
Attribute werden als Teil der so genannten Metadaten eines Programms bzw. einer Bibliothek gespeichert. (Die Metadaten beschreiben die äußeren Merkmale aller Klassen, Methoden, Eigenschaften einer ausführbaren .NET-Programmdatei. Der Objektbrowser ist ein Werkzeug, um diese Metadaten in einer verständlichen Form anzuzeigen. Sie können diese Metadaten mit den Klassen des System.Reflection-Namensraum auslesen.)
7.7 Attribute
273
VERWEIS
Aus diesem Grund können Attribute losgelöst von der Programmausführung ausgewertet werden. Beispielsweise haben der Compiler, der Debugger und die Entwicklungsumgebung Zugriff auf die Attribute. Es überrascht daher kaum, dass es eine ganze Reihe von .NET-Attributen gibt, die zur Steuerung des Compilers, Debuggers etc. dienen. In den .NET-Bibliotheken sind zahllose Attribute definiert, mit denen Sie diverse Spezialeffekte erzielen können. Es gibt allerdings keine gesammelte Dokumentation über alle zur Verfügung stehenden Attribute. Einen ersten Überblick geben die Informationen im Rahmen der VB.NET-Sprachdefinition in der Online-Hilfe. Noch viel mehr Attribute entdecken Sie, wenn Sie im Objektbrowser nach Klassen suchen, die mit ...Attribute enden. ms-help://MS.VSCC/MS.MSDNVS.1031/vbcn7/html/vaconExamplesOfCustomAttributeUsage.htm
Konkrete Anwendungsbeispiele für Attribute in diesem Buch finden Sie mit Hilfe des Stichwortverzeichnis (Schlagwort Attribute). Insgesamt war es für die in diesem Buch vorgestellten Beispiele nur recht selten erforderlich, Attribute einzusetzen.
Attribute angeben Attribute werden in VB.NET in spitze Klammern gesetzt und der eigentlichen Deklaration vorangestellt. Bei vielen Attributen reicht einfach deren Nennung, um das Attribut zu aktivieren; bei manchen Attributen müssen Sie darüber hinaus Parameter in der Form para:=wert angeben. Die folgenden Zeilen definieren eine Konstantenaufzählung, bei der die einzelnen Konstanten bitweise kombiniert werden dürfen (siehe auch Abschnitt 4.4.2). Enum myPrivileges As Integer ReadAccess = 1 WriteAccess = 2 Execute = 4 End Enum
Die meisten Attributnamen enden mit Attribute. Diese Endung muss allerdings nicht angegeben werden. (Aus diesem Grund finden Sie das Flags-Attribut im Objektbrowser oder in der Online-Hilfe nur unter den Namen FlagsAttribute!) Ein Sonderfall sind Attribute für die Assembly bzw. für eine einzelne Datei der Assembly (ein .NET-Modul): Diese Attribute werden in der Form bzw. angegeben.
Eigene Attribute definieren Sie können auch selbst Attribute definieren. Im einfachsten Fall müssen Sie dazu nur eine eigene Klasse von System.Attributes ableiten. Die folgenden Zeilen definieren ein neues Attribut, mit dem Sie eine Kommentarzeichenkette angeben können.
274
7 Objektorientierte Programmierung
' Beispiel oo-programmierung\attributes-intro Public Class CommentAttribute Inherits Attribute Public Text As String End Class
Bei der Anwendung dieses Attributs können Sie die öffentlichen Klassenelemente in der Form para:=wert initialisieren. _ Class class1 ... Klassencode End Class
Attribute auswerten Wenn Sie feststellen möchten, mit welchen selbst definierten Attributen eine Klasse ausgestattet ist, ermitteln Sie mit GetType das Type-Objekt zur Beschreibung der Klasse. Darauf wenden Sie die Methode GetCustomAttributes an. Diese Methode liefert ein Object-Feld zurück. Mit TypeOf können Sie den tatsächlichen Objekttyp feststellen und gegebenenfalls zur Auswertung eine Umwandlung in die eigenen CommentAttribute-Klasse durchführen. ' Beispiel oo-programmierung\attributes-intro Sub Main() Dim x As New class1() Dim obj As Object For Each obj In x.GetType().GetCustomAttributes(False) Console.WriteLine(obj.ToString) If TypeOf obj Is CommentAttribute Then Console.WriteLine(" Text={0}", _ CType(obj, CommentAttribute).Text) End If Next End Sub
7.8
Namensräume
Wie Sie sicher schon festgestellt haben, sind die Namen der .NET-Klassen aus vielen, jeweils durch Punkte getrennten Teilen zusammengesetzt: System.IO.FileInfo, System.Windows.Forms.Button etc. (Vielleicht ist die Abkürzung .NET für dotnet ja eine Referenz auf die Allgegenwart des Punkts?) Der erste Teil des Klassennamens wird als Namensraum bezeichnet. Bei den beiden Beispielen lautet der Namensraum also System.IO bzw. System.Windows.Forms.
HINWEIS
7.8 Namensräume
275
Beachten Sie, dass der eigentliche Klassenname ebenfalls mehrteilig sein kann! Bei System.Windows.Forms.ListView.ListViewItemCollection lautet der Klassenname ListView.ListViewItemCollection, der Namensraum ist abermals nur System.Windows.Forms.
Grundsätzlich wäre es natürlich auch möglich gewesen, allen Klassen einteilige Namen zu geben – also nur FileInfo, Button etc. Die mehrteiligen Namen haben aber Vorteile: •
Es besteht die Möglichkeit, gleichnamige Klassen in unterschiedlichen Namensräumen zu definieren, ohne dass es dabei zu Konflikten kommt.
•
Zusammengehörende Klassen können in einem Namensraum gruppiert werden. Angesichts der Tatsache, dass die .NET-Klassenbibliothek bereits mehrere Tausend Klassen enthält, vergrößert diese Möglichkeit zur hierarchischen Gliederung die Übersicht ganz erheblich.
Soweit es um die Anwendung von .NET-Klassen geht, müssen Sie bei der Deklaration von Variablen oder Parametern entweder den vollständigen Klassennamen angeben oder Sie müssen auf die in Abschnitt 6.2.2 ausführlich beschriebene Anweisung Imports zurückgreifen, um den Tippaufwand zu minimieren.
HINWEIS
Namensräume haben nichts mit Bibliotheken, Programmen oder Assemblies zu tun: So können unterschiedliche Bibliotheken und Klassen für denselben Namensraum zur Verfügung stellen. (Beispielsweise enthalten die Bibliotheken mscorlib.dll und System.dll beide Klassen für den Namensraum System.IO.) Umgekehrt kann ein Programm bzw. eine Bibliothek durchaus Klassen in mehreren Namensräumen zur Verfügung stellen. Beispielsweise sind die in mscorlib.dll enthaltenen Klassen auf mehr als 30 Namensräume verteilt. Der Objektbrowser gruppiert alle Klassen zuerst nach Bibliotheken, dann nach Namensräumen und zuletzt nach den darin enthaltenen Klassen, Aufzählungen etc. Diese Hierarchie ist willkürlich. Der Objektbrowser könnte ebensogut als erste Hierarchieebene den Namensraum wählen und alle dafür verfügbaren Klassen anzeigen, unabhängig davon, aus welcher Bibliothek die Klassen stammen.
Namensraum für eigene Klassen definieren Bei einem eigenen Projekt ergibt sich der Defaultnamensraum für alle Klassen und Module per Default aus dem Projektnamen. Die Entwicklungsumgebung trägt diesen Namen automatisch in das Feld STAMMNAMESPACE des Projekteigenschaftsdialogs ein, wobei für den Namensraum ungültige Zeichen durch andere Zeichen ersetzt oder ganz entfernt werden.
VORSICHT
276
7 Objektorientierte Programmierung
Verwenden Sie nach Möglichkeit keinen in der .NET-Bibliothek vorkommenden Namen einer Klasse oder eines Namensraums als Projektnamen! Wenn Sie Ihr Projekt Button nennen und dann versuchen, die Button-Klasse der Windows.Forms-Bibliothek zu verwenden, gibt es unweigerlich Probleme!
Für den Namensraum gelten im Wesentlichen dieselben Regeln wie für Variablennamen: Erlaubte Zeichen sind alle Buchstaben und Zahlen sowie das Zeichen _. Der Stammnamensraum darf auch aus mehreren Teilen zusammengesetzt werden – etwa abc.def. Nicht zulässig sind die meisten Sonderzeichen, darunter auch der Bindestrich. Wenn Sie im Projekt myprojekt eine Klasse class1 definieren, so lautet deren vollständiger Name myprojekt.class1. Dieser Name ist allerdings nur dann relevant, wenn das Projekt eine Klassenbibliothek ist, die Sie in einem anderen Programm nutzen möchten. Innerhalb des Projekts können Sie auf alle darin definierten Klassen unmittelbar zugreifen. Mit den Anweisungen Namespace name und End Namespace bilden Sie eine neue Namensraumuntergruppe. Der angegebene Name gilt als Ergänzung zum Stammnamensraum, wobei der Punkt zur Trennung der Namensteile nicht angegeben werden darf. NamespaceAnweisungen müssen direkt auf Dateiebene vorgenommen werden. (Namespace darf also nicht innerhalb einer Klasse oder eines Moduls verwendet werden.) Namespace-Anweisungen dürfen ineinander verschachtelt werden.
Beispiel Die folgenden Zeilen demonstrieren die Anwendung der Namespace-Anweisung. Als Stammnamensraum für das Projekt wurde bei den Projekteigenschaften mynamespace angegeben. Beachten Sie insbesondere, dass sich class3 und class4 im selben Namensraum befinden! Innerhalb des Projekts können die Klassen unter den Namen class1, abc.class2 etc. angesprochen werden. Wenn das Projekt dagegen zu einer Bibliothek (DLL-Datei) kompiliert wird und in einem anderen Programm genutzt wird, dann lauten die Klassennamen mynamespace.class1, mynamespace.abc.class2 etc. (Wenn Sie möchten, können Sie diese langen Namen auch innerhalb des Projekts verwenden.) ' Beispiel oo-programmierung\namespace-intro ' der Stammnamensraum lautet mynamespace Module Module1 Sub Main() Dim x1 As New class1() 'oder mynamespace.class1 Dim x2 As New abc.class2() 'oder mynamespace.abc.class2 Dim x3 As New abc.efg.class3() Dim x4 As New abc.efg.class4() Console.WriteLine(x1.GetType.FullName) 'liefert mynamespace.class1 End Sub End Module Class class1 End Class
7.9 Gültigkeitsbereiche (scope)
277
Namespace abc Class class2 End Class Namespace efg Class class3 End Class End Namespace End Namespace Namespace abc.efg Class class4 End Class End Namespace
7.9
Gültigkeitsbereiche (scope)
VERWEIS
Unter welchen Umständen können Sie in Klasse A auf eine Variable in Klasse B zugreifen? Wie kann eine Methode der Klasse C vom Modul D genutzt werden? Dieser Abschnitt gibt Antwort auf diese Fragen, stellt die Schlüsselwörter zur Erweiterung bzw. zur Einschränkung des Gültigkeitesbereichs vor und gibt an, welche Einstellungen per Default gelten. Einen ausgezeichneten (englischen) Artikel zu diesem Thema finden Sie auf den Microsoft-Entwicklerseiten, wenn Sie nach Variable and Method Scope suchen: http://msdn.microsoft.com/library/en-us/dndotnet/html/methodscope.asp
7.9.1
Gültigkeitsbereich definieren
Zur Definition des Gültigkeitsbereichs eines VB.NET-Konstrukts (Variable, Prozedur, Methode, Eigenschaft etc.) können Sie der Deklaration eines der folgenden Schlüsselwörter voranstellen. •
Private bedeutet, dass das Konstrukt (Variable, Prozedur, Methode, Eigenschaft etc.) nur innerhalb der Gültigkeitsebene verwendet werden kann, in der das Konstrukt definiert ist.
Wenn also eine Variable mit Private Dim a innerhalb einer Klasse deklariert ist, dann ist diese Variable von außen hin vollkommen unzugänglich. •
Protected hat fast diesselbe Wirkung wie Private. Der einzige Unterschied besteht darin, dass das Konstrukt auch in vererbten Klassen zugänglich ist.
Wenn Sie also in Klasse X die Anweisung Protected b ausführen und dann Klasse Y von X ableiten, können Sie im Code der Klasse Y auf b zugreifen.
278
•
7 Objektorientierte Programmierung
Friend bedeutet, dass das Konstrukt innerhalb des aktuellen Projekts generell zugänglich ist, aber nicht nach außen hin. Friend ist vor allem bei der Programmierung von
Klassenbibliotheken wichtig. Wenn Sie in der Klasse X die Anweisung Friend c ausführen, können Sie in allen anderen Klassen des aktuellen Projekts darauf zugreifen. Das gilt sowohl für den Code vererbter Klassen als auch für alle Objekte der Klasse X, die in irgendwelchen anderen Klassen oder Modulen verwendet werden. Wenn Sie die aus dem Projekt resultierende Klassenbibliothek aber in einem anderen Projekt einsetzen, ist die Variable c dort unzugänglich (als wäre sie mit Private deklariert). •
Protected Friend kombiniert die Funktionen von Protected und Friend.
•
Public macht das Konstrukt global zugänglich, also sowohl innerhalb des aktuellen
HINWEIS
Projekts als auch außerhalb (wenn das Projekt als Klassenbibliothek eingesetzt wird). Beachten Sie, dass Codedateien keinen Einfluss auf die Gültigkeitsbereiche von VB.NET-Konstrukten haben. Für den Zugriff auf Variablen, den Aufruf von Prozeduren etc. spielt es keine Rolle, ob Klasse A in ClassA.vb und Klasse B in ClassB.vb definiert sind oder ob beide Klassen in derselben Datei definiert sind. Ebenso ist der Dateiname der Codedateien unerheblich.
Beispiel Ausgangspunkt für die folgenden Überlegungen ist die Klasse ClassX. ' Beispiel oo-programmierung\protected-var Class ClassX Private a As Integer Protected b As Integer Friend c As Integer Protected Friend d As Integer Public e As Integer End Class
Wenn Sie im selben Projekt ein Objekt der Klasse ClassX erzeugen, können Sie auf die Variablen c, d und e zugreifen. Dim ox As New ClassX()
'hier zugänglich: ox.c, ox.d, ox.e
Wenn Sie im selben Projekt die Klasse ClassY von ClassX ableiten, können Sie darin auf die Variablen b, c, d und e zugreifen.
7.9 Gültigkeitsbereiche (scope)
279
Class ClassY Inherits ClassX Sub do_something() ' hier zugänglich: b, c, d und e End Sub End Class
Wenn Sie in einem anderen Projekt die Klassenbibliothek mit der Definition von ClassX nutzen und dort eine Objekt dieser Klasse erzeugen, können Sie nur auf die Variable e zugreifen. ' in einem externen Projekt Dim ox As New ClassX() 'hier zugänglich: ox.c, ox.d, ox.e
Wenn Sie in einem externen Projekt ClassX vererben, können Sie auf die Variablen b, d und e zugreifen. ' in einem externen Projekt Class ClassZ Inherits ClassX Sub do_something() ' hier zugänglich: b, d und e End Sub End Class
7.9.2
Defaultgültigkeit
Sie können Variablen, Funktionen, Eigenschaften etc. auch ohne Angabe eines der obigen Schlüsselwörter deklarieren. Das ist der Regelfall in vielen VB.NET-Programmen. Die folgenden Tabellen geben die Defaultgültigkeit diverser VB.NET-Konstrukte in Abhängigkeit vom Deklarationsort an. Beachten Sie insbesondere, dass Variablen in Modulen und Klassen anders behandelt werden als in Strukturen! Defaultgültigkeitsbereich in Codedateien Module
Friend Module
Class
Friend Class
Structure
Friend Struct
Enum
Friend Enum
Defaultgültigkeitsbereich in Modulen Class/Structure/Enum
Public Class/Structure/Enum Class
Function/Sub
Public NotOverridable Function/Sub
Event/Delegate
Public Event/Delegate
Dim/Const
Private Dim/Const
280
7 Objektorientierte Programmierung
Defaultgültigkeitsbereich in Klassen Class/Structure/Enum
Public Class/Structure/Enum
Function/Sub/Property
Public NotOverridable Function/Sub/Property
Event/Delegate
Public Event/Delegate
Dim/Const
Private Dim/Const
Defaultgültigkeitsbereich in Strukturen Class/Structure/Enum
Public Class/Structure/Enum
Function/Sub/Property
Public NotOverridable Function/Sub/Property
Event/Delegate
Public Event/Delegate
Dim/Const
Public Dim/Const
TIPP
Bei der Deklaration von Variablen und Konstanten innerhalb von Prozeduren, Methoden oder Eigenschaften dürfen keine Gültigkeitsbezeichnungen angegeben werden. Die Variablen bzw. Konstanten gelten als lokal, d.h., sie können nur innerhalb der Prozedur verwendet werden. (Variablen können optional als Static deklariert werden. Das bedeutet, dass sie ihren Inhalt zwischen Prozeduraufrufen behalten und dass ihr Inhalt im Code und nicht wie sonst üblich am Stapelspeicher gespeichert wird. Static wird in Abschnitt 5.3.2 näher vorgestellt.) Wie können Sie die Defaultgültigkeit von VB.NET-Konstrukten feststellen? Die naheliegende Antwort wäre vielleicht die Online-Hilfe – aber dort suchen Sie lang und wahrscheinlich vergeblich. Es geht aber viel einfacher: Geben Sie im Codefenster die Deklaration einer Variablen, Prozedur etc. ohne Gültigkeitsbezeichner an und werfen Sie dann einen Blick in die Klassenansicht oder in den Objektbrowser. Dort können Sie die vollständige (interne) Deklaration des Schlüsselworts herausfinden. (Bei der Klassenansicht müssen Sie dazu die Maus über das Icon des Schlüsselworts bewegen – dann erscheint die Deklaration des Schlüsselworts in einem kleinen, gelben ToolTip-Fenster.) Zum Experimentieren können Sie das hier nicht abgedruckte Beispielprogramm ooprogrammierung\scope verwenden. Das Programm erfüllt keine konkrete Aufgabe, enthält aber unzählige, ineinander verschachtelte Deklarationen von Modulen, Klassen etc.
7.10 Syntaxzusammenfassung
7.10
281
Syntaxzusammenfassung
Konstrukte der objektorientierten Programmierung Class name End Class
deklariert eine Klasse.
Module name End Module
deklariert ein Modul.
Structure name End Structure
deklariert eine Datenstruktur.
Enum name End Enum
deklariert eine Konstantengruppe (Aufzählung).
Interface name End Interface
deklariert eine Schnittstelle.
Inherits basisklasse Inherits basisschnittstelle
gibt an, dass die Klasse von basisklasse vererbt wird. Inherits muss am Beginn einer Klassendefinition angegeben werden. Inherits kann auch am Beginn einer Schnittstellendeklaration
angegeben werden. Die neue Schnittstelle erbt damit alle Merkmale der Basisschnittstelle. Bei Schnittstellen ist sogar eine Vererbung mehrerer Basisschnittstellen möglich (während Klassen nur von einer Basisklasse erben können). Implements schnittstelle
gibt an, dass die Klasse oder die Struktur die angegebene Schnittstelle implementiert. Implements muss am Beginn einer Klassen- oder Strukturdefinition angegeben werden (aber nach Inherits).
stellt der Deklaration eine Attributangabe voran.
Elemente von Klassen Dim/Const x
deklariert eine Klassenvariable bzw. Konstante.
Sub/Function m(...)
deklariert eine Prozedur (privat) oder eine Methode (öffentlich).
Property p() As String Get ... Return wert End Get Set(Value As String) ... End Set End Property
deklariert eine Eigenschaft. Der Get-Teil wird ausgeführt, wenn die Eigenschaft p gelesen wird, der Set-Teil, wenn sie verändert wird. Dabei wird der neue Wert im Parameter Value übergeben. Wenn die Eigenschaft als ReadOnly deklariert ist, entfällt der Set-Teil. Analog entfällt der Get-Teil, wenn die Eigenschaft als WriteOnly deklariert wurde.
282
7 Objektorientierte Programmierung
Elemente von Klassen ... Implements interface.member
gibt an, dass die voranstehende Eigenschaft oder Methode bzw. das Ereignis zur Realisierung einer Schnittstelle dienen.
Zugriff auf die aktuelle Instanz innerhalb des Klassencodes Me
verweist auf die aktuelle Instanz der Klasse.
MyBase
verweist ebenfalls auf die aktuelle Instanz, ermöglicht aber die Verwendung von Schlüsselwörtern der Basisklasse.
MyClass
verweist ebenfalls auf die aktuelle Instanz, erzwingt aber in jedem Fall die Verwendung von Eigenschaften oder Methoden, die in der aktuellen Klasse definiert sind (selbst dann, wenn aufgrund des Objekttyps eigentlich in einer abgeleiteten Klasse definierte Overrides-Eigenschaften oder -Methoden aufgerufen werden müssten).
Namensraum Namespace abc End Namespace
ergänzt den Namensraum um .abc. (Als Ausgangspunkt gilt der Stammnamensraum aus den Projekteigenschaften.) Die Einstellung gilt für alle darin deklarierten Module, Klassen etc.
Imports name
gibt an, dass bei Deklarationen von Variablen, Parametern etc. der Klassenname im angegebenen Namensraum gesucht werden soll. (Beachten Sie, dass weitere Importe in den Projekteigenschaften eingestellt werden können.)
Ereignisse und Delegates Ereignisse Event ev(parameter)
deklariert ein Ereignis (z.B. innerhalb des Codes einer Klasse).
Delegate Sub deleg(parameter) Event ev As deleg
deklariert ein Ereignis, wobei die Parameterliste nicht direkt, sondern durch eine Delegate-Definition angegeben wird.
RaiseEvent ev(argumente)
löst ein Ereignis aus (ebenfalls innerhalb der Klasse).
Dim WithEvents obj1 _ As New class1() Sub name1(parameter) _ Handles obj1.ev
name1 verarbeitet das Ereigniss ev des Objekts obj1 der Klasse class1. Die Prozedur wird jedes Mal aufgerufen, wenn innerhalb der Klasse RaiseEvent ausgeführt wird.
7.10 Syntaxzusammenfassung
283
Ereignisse Dim obj2 As New class1() AddHandler obj2.ev, _ AddressOf name2 Sub name2(parameter)
name2 verarbeitet das Ereignis ev des Objekts obj2 der Klasse class1. Diese Vorgehensweise ist eine syntaktische
RemoveHandler obj2.ev, _ AddressOf name2
deaktiviert die Ereignisprozedur name2 für das Ereignis obj2.ev.
Alternative zur obigen Variante.
Delegates Delegate Sub d1(parameter) Delegate Function d2(para) _ As typ
deklariert eine Delegate-Klasse zum Aufruf einer Prozedur oder Funktion mit den angegebenen Parametern und Rückgabewerten.
Dim ptr As d2
deklariert ptr als Variable für ein Objekt der Delegate-Klasse d2.
ptr = AddressOf func
speichert in ptr den Zeiger auf die Funktion func. (Genau genommen wird nicht einfach ein Zeiger, sondern ein Delegate-Objekt gespeichert.)
result = ptr.Invoke(parameter)
ruft die Funktion func auf.
Optionale Kennzeichner Optionale Kennzeichner zur Deklaration des Gültigkeitsbereichs Private
beschränkt die Gültigkeit auf das Klasseninnere.
Protected
erlaubt den Zugriff nur in vererbten Klassen.
Friend
erlaubt den Zugriff nur im Code desselben Projekts. Friend kann mit Protected kombiniert werden.
Public
macht das Element öffentlich.
Die folgenden Kennzeichner können nur für bestimmte Elemente einer Klasse verwendet werden, beispielsweise Default nur für Eigenschaften. Das oder die Elemente sind in der zweiten Spalte fett hervorgehoben. Optionale Kennzeichner von Konstrukten und Elementen Default
gibt an, dass die Eigenschaft die Defaulteigenschaft der Klasse ist. Das ermöglicht einen Zugriff auf diese Eigenschaft ohne explizite Nennung.
284
7 Objektorientierte Programmierung
Optionale Kennzeichner von Konstrukten und Elementen MustInherit
gibt an, dass die Klasse nicht direkt genutzt werden kann (dass also kein Objekt dieser Klasse erzeugt werden kann). Die Klasse kann damit ausschließlich als Basisklasse für die Definition einer anderen Klasse dienen.
MustOverride
gibt an, dass die Eigenschaft oder Methode nicht direkt verwendet werden kann, sondern in einer abgeleiteten Klasse durch eine eigene Implementierung mit Overrides ersetzt werden muss.
NotInheritable
gibt an, dass diese Klasse nicht vererbt werden kann.
NotOverridable
gibt an, dass die Eigenschaft oder Methode in einer abgeleiteten Klasse nicht durch Overrides ersetzt werden darf.
Overloads
gibt an, dass die Eigenschaft oder Methode eine von der Basisklasse vorgegebene Eigenschaft oder Methode ersetzt. Die Ersetzung gilt nur, solange der Compiler dem Objekt die richtige Klasse zuordnet. Overloads kann nicht verwendet werden, wenn sich der Rückgabetyp ändert.
Overridable
gibt an, dass die Eigenschaft oder Methode durch eine vererbte Klasse ersetzt werden darf.
Overrides
gibt an, dass die Eigenschaft oder Methode eine von der Basisklasse vorgegebene Eigenschaft oder Methode ersetzt. Overrides kann nur verwendet werden, wenn das Basisschlüsselwort als Overridable gekennzeichnet ist, wenn die Datentypen der Parameter bzw. des Rückgabewerts unverändert bleiben und die Gültigkeitsebene nicht erweitert wird.
ReadOnly
gibt an, dass die Eigenschaft oder Klassenvariable nur gelesen, aber nicht verändert werden darf.
Shadows
gibt an, dass ein Klassenmitglied in einer vererbten Klasse alle gleichnamigen Klassenmitglieder der Basisklasse überdeckt. Die Überdeckung gilt nur, solange der Compiler dem Objekt die richtige Klasse zuordnet. Shadows erlaubt auch die Überdeckung unterschiedlicher Klassenmitglieder (z.B., dass eine Eigenschaft eine Variable überdeckt).
Shared
gibt an, dass die Klassenvariable zwischen allen Objekten der Klasse geteilt wird bzw. dass die Eigenschaft oder Methode ohne eine Instanz der Klasse verwendet werden kann.
Static
gibt an, dass die Prozedurvariable bei mehrfachen Prozeduraufrufen ihren Wert behalten soll.
7.10 Syntaxzusammenfassung
Optionale Kennzeichner von Konstrukten und Elementen WriteOnly
gibt an, dass die Eigenschaft nur verändert, aber nicht gelesen werden darf.
285
Teil III
Programmiertechniken
8
Zahlen, Zeichenketten, Datum und Uhrzeit
Dieses Kapitel beschreibt ausführlich den Umgang mit Zahlen, Zeichenketten, Datum und Uhrzeit. Vielleicht fragen Sie sich, was es über dieses Thema viel zu schreiben gibt. Aber die Fülle neuer Datentypen und eine Unzahl von Methoden, die bei der Manipulation, Umwandlung und Formatierung helfen, fordern auch beim Programmierer ihren Tribut: nicht weil die Anwendung schierig wäre, sondern weil es gilt, den Überblick zu bewahren und für eine bestimmte Aufgabe die richtige Methode auszuwählen. Das Kapitel geht auch auf Typenkonvertierung ein, die VB.NET in manchen Fällen automatisch durchführt und die Sie ansonsten selbst initiieren müssen. Das Thema ist zwar bei den in der Kapitelüberschrift genannten Datentypen besonders wichtig, spielt aber auch bei allen anderen Datentypen und Klassen eine große Rolle. 8.1 8.2 8.3 8.4 8.5 8.6
Zahlen Zeichenketten Datum und Uhrzeit Konvertierung zwischen Datentypen .NET-Formatierungsmethoden VB-Formatierungsmethoden
290 298 319 331 339 349
290
8 Zahlen, Zeichenketten, Datum und Uhrzeit
8.1
Zahlen
8.1.1
Notation von Zahlen
Fließkommazahlen: Bei Fließkommazahlen müssen Sie im Programmcode immer einen Punkt zur Dezimaltrennung verwenden (also 3.1415 statt 3,1415). Sehr große oder sehr kleine Fließkommazahlen können Sie im Programmcode in der wissenschaftlichen Notation angeben, also etwa 3.1E10, wenn Sie 3.1*1010 meinen. Visual Basic ersetzt diese Eingabe dann automatisch durch 31000000000. Nur bei wirklich großen Zahlen belässt Visual Basic es bei der Dezimalnotation (3.1E+50). Hexadezimale Schreibweise: Normalerweise interpretiert Visual Basic Zahlen natürlich im dezimalen System. Nur wenn einer Zahl &H oder &O vorangestellt wird, betrachtet Visual Basic diese Zahl als hexadezimal bzw. oktal. Dim i = i = i = i =
i As Integer, s As String &H10 'i = 16 &HFFF0 'i = 65520 &HFFFFFFF0 'i = -16 &O10 'i = 8
Für Konvertierungen in die umgekehrte Richtung dienen die Funktionen Hex und Oct. Sie liefern Zeichenketten von Zahlen in hexadezimaler bzw. oktaler Schreibweise (ohne vorangestelltes &H bzw. &O). s = Hex(100) s = Hex(-100) s = Oct(100)
's = "64" 's = "FFFFFF9C" 's = "144"
Literale Ganze Zahlen im Programmcode gelten immer als Integer-Zahlen, Fließkommazahlen immer als Double-Zahlen. Sie können Zahlen aber auch explizit einen Datentyp zuweisen, indem Sie einen Buchstaben hintanstellen. So gilt 23L etwa als Long-Zahl. Besonders deutlich bemerken Sie den Unterschied, wenn Sie negative Zahlen mit Hex in die hexadezimale Schreibweise umwandeln: Hex(-3) liefert FFFFFFFD (Integer). Hex(-3S) liefert FFFD (Short). Hex(-3L) liefert FFFFFFFFFFFFFFFD (Long).
Sie können den Datentyp natürlich mit TypeName überprüfen: TypeName(3!) liefert beispielsweise Single.
8.1 Zahlen
291
Literale zur Kennzeichnung von Datentypen C
Char (einzelnes Unicode-Zeichen, z.B. "x"c)
D oder @
Decimal (Festkommazahl mit 28 Stellen Genauigkeit)
F oder !
Single (Fließkommazahl mit 8 Stellen Genauigkeit)
I oder %
Integer (32-Bit-Integer mit Vorzeichen, Default bei ganzen Zahlen)
L oder &
Long (64-Bit-Integer mit Vorzeichen)
R oder #
Double (Fließkommazahl mit 16 Stellen Genauigkeit, Default bei
Fließkommazahlen) S
8.1.2
Short (16-Bit-Integer mit Vorzeichen)
Rundungsfehler bei Fließkommazahlen
Prinzipbedingt treten bei den Datentypen Double und Single immer Rundungsfehler auf. Diese Fehler resultieren aus der internen Darstellung der Zahlen und sind nichts, was Sie Microsoft vorwerfen können. (Die Rundungsfehler treten nur im Bereich der Rechengenauigkeit auf – im folgenden Beispiel an der 16ten Nachkommastelle.) Rundungsfehler lassen sich nur verhindern, indem andere Datentypen eingesetzt werden – je nach Anwendung z.B. Integer, Long, Decimal. Vergessen Sie aber nicht, dass Berechnungen mit Decimal-Zahlen viel langsamer sind als mit Double-Zahlen! Im folgenden Beispielprogramm wird zweimal eine Schleife von -1 bis 1 mit einer Schrittweite von 0.1 durchlaufen. Dabei wird als Schleifenvariable zuerst eine Decimal, dann eine Double-Variable verwendet. Abbildung 8.1 beweist, dass im zweiten Fall im Bereich um 0 offensichtliche Rundungsfehler auftreten.
Abbildung 8.1: Rundungsfehler bei der Verwendung von Double-Variablen
292
8 Zahlen, Zeichenketten, Datum und Uhrzeit
HINWEIS
' Beispielprogramm zahlen-zeichenketten/rechengenauigkeit Option Strict On Module Module1 Sub Main() Dim dec As Decimal, dbl As Double ' Schleife mit Decimal-Variable Console.WriteLine("------ mit Decimal -----") For dec = -1 To 1 Step 0.1D Console.Write(dec & " ") Next Console.WriteLine() Console.WriteLine() ' Schleife mit Double-Variable Console.WriteLine("------ mit Double -----") For dbl = -1 To 1 Step 0.1 Console.Write(dbl & " ") Next End Sub End Module
Beachten Sie, dass in der ersten Schleife die Schrittweite 0.1 mit dem Literal D als Decimal-Zahl gekennzeichnet wurde. Vergessen Sie das, kann es sein, dass bereits an dieser Stelle Rundungsfehler auftreten! (Wenn Sie wie der Autor Option Strict verwenden, erinnert Sie die Entwicklungsumgebung bzw. der Compiler an solche kleinen Ungenauigkeiten.)
8.1.3
Division durch null und der Wert unendlich
Die Datentypen Single und Double (nicht aber Decimal) kennen die Werte unendlich bzw. minus unendlich. Bei einer Division durch null (x = y / 0.0) tritt deswegen kein Fehler auf! Stattdessen enthält x nun Double.NegativeInfinity bzw. Double.NegativeInfinity. Diese Werte können mit den Methoden IsInfinity, IsNegativeInfinity bzw. IsPositiveInfinity festgestellt bzw. als -1.#INF bzw. 1.#INF ausgegeben werden. Die folgende Tabelle fasst diese und einige weitere Eigenschaften und Methoden zusammen, die zur Verarbeitung von unendlich bzw. NaN (not a number). Um zu testen, ob die Double-Variable x einen ungültigen Wert enthält, müssen Sie Double.IsNaN(x) ausführen. (Die näher liegende Form x.IsNaN() ist leider nicht vorgesehen.)
8.1 Zahlen
293
Eigenschaften und Methoden von System.Single und System.Double Epsilon
enthält die kleinste darstellbare Zahl, die noch größer als 0 ist (bei Double ca. 4.94065645841247E-324).
MaxValue
enthält die größte darstellbare Zahl (bei Double ca. 1.7976931348623157E+308)
MinValue
enthält die kleinste darstellbare Zahl.
NaN
gibt einen Wert an, der zur Repräsentierung irregulärer Zustände verwendet wird (not a number, -1.#IND).
NegativeInfinity
gibt den Wert minus unendlich an (-1.#INF).
PositiveInfinity
gibt den Wert unendlich an (1.#INF).
IsInfinity
testet, ob der Wert unendlich enthält (egal mit welchem Vorzeichen).
IsNaN
testet, ob es sich um einen ungültigen Wert handelt.
IsNegativeInfinity
testet, ob der Wert negativ unendlich ist.
IsPositiveInfinity
testet, ob der Wert positiv unendlich ist.
8.1.4
Arithmetische Funktionen
Arithmetische Funktionen sind Teil der Systems.Math-Klasse. Statt Sin(x) in VB6 heißt es daher nun Math.Sin(x). Wenn Sie häufig arithmetische Funktionen einsetzen, sollten Sie die Anweisung Imports System.Math verwenden: dann können Sie arithmetische Funktionen wieder wie in VB verwenden. Alle Math-Funktionen erwarten Double-Parameter und liefern Double-Ergebnisse. System.Math – Arithmetische Funktionen und Konstanten E
Eulersche Zahl e (2.71828182845905)
Pi
Kreisteilungszahl π
Abs(x)
Absolutbetrag
Acos(x), Asin(x), Atan(x)
Arcussinus, -cosinus, -tangens
Atan2(x, y)
Arcustangens zu x/y
Cos(x), Sin(x), Tan(x)
Sinus, Cosinus, Tangens
Cosh(x), Sinh(x), Tanh(x)
hyperbolische Funktionen
Exp(x)
Exponentialfunktion (e )
Log(x)
natürlicher Logarithmus zur Basis e
Log10(x)
Logarithmus zur Basis 10
x
294
8 Zahlen, Zeichenketten, Datum und Uhrzeit
System.Math – Arithmetische Funktionen und Konstanten Log(x, b)
Logarithmus zur Basis b
Pow(x, y)
berechnet x (entspricht in VB.NET x^y)
Sign(x)
Signum-Funktion (liefert 1 bei positiven Zahlen, 0 bei 0, -1 bei negativen Zahlen)
Sqrt(x)
Quadratwurzel
TIPP
In der Klasse Microsoft.VisualBasic.Financial stehen einige finanzmathematische Funktionen zur Verfügung. Diese Funktionen können direkt verwendet werden (also ohne vorangestelltes Math).
VORSICHT
y
Anders als in VB6 und vielen anderen Programmiersprachen führen ungültige Berechnungen nicht zu einem Fehler. Math.Sqrt(-1) oder Math.Log(-1) liefern als Ergebnis den Double-Wert -1.#IND. Dieser Wert entspricht dem Zustand not a number, der mit Double.IsNaN(x) festgestellt werden kann.
8.1.5
Zahlen runden und andere Funktionen
VB enthält zwei eigene Rundungsfunktionen: Fix und Int. Fix schneidet einfach den Nachkommaanteil ab. Int verhält sich bei positiven Zahlen gleich, rundet aber bei negativen Zahlen ab. Die Besonderheit dieser beiden Funktionen besteht darin, dass sie für verschiedene Datentypen definiert sind. Wenn Sie an Int beispielsweise einen Double-Parameter übergeben, liefert diese Funktion auch das Ergebnis als Double-Zahl. Übergeben Sie dagegen einen Decimal-Parameter, ist auch das Ergebnis vom Typ Decimal. Microsoft.VisualBasic.Conversion-Methoden Fix(x)
schneidet den Nachkommaanteil ab: Fix(2.9) liefert 2. Fix(-2.9) liefert -2.
Int(x)
rundet zur nächst kleinern Zahl ab: Int(2.9) liefert 2. Int(-2.1) liefert -2.
Die meisten anderen Funktionen, die zum Runden und für vergleichbare Zwecke geeignet sind, befinden sich in System.Math. Diese Funktionen erwarten durchweg Double-Parameter und liefern Double-Ergebnisse. Math.Floor entspricht im Verhalten exakt Int. Der einzige wesentliche Unterschied liegt bei den Datentypen: Floor erwartet einen Double-Parameter und liefert auch das Ergebnis als Double-Zahl. Math.Ceiling funktioniert so ähnlich wie Floor, rundet aber immer auf.
8.1 Zahlen
295
Die einzige Funktion, die wirklich im Sinne des Sprachgebrauchs rundet, ist Math.Round. Hier wird bei einen Nachkommaanteil kleiner 0,5 abgerundet, bei einem Nachkommaanteil größer 0,5 dagegen aufgerundet. Auf den ersten Blick eigentümlich ist das Verhalten allerdings bei einem Nachkommaanteil von genau 0,5: Dort rundet Round zur nächsten geraden (!) Zahl: 1.5 wird ebenso wie 2.5 zu 2 gerundet. (Dieses Verhalten entspricht also nicht ganz der Schulmathematik, in der bei 0,5 immer aufgerundet wird. Der Vorteil von Round besteht aber darin, dass die Summe der Fehler, die beim Runden vieler gleichverteilter Zahlen entsteht, gegen 0 geht.) System.Math – Sonstige Funktionen Ceiling(x)
rundet zur nächstgrößeren ganzen Zahl auf: Ceiling(0.1) liefert 1. Ceiling(-2.9) liefert -2. Ceiling(3) liefert unverändert 3.
Floor(x)
rundet zur nächstkleineren ganzen Zahl ab: Floor(0.1) liefert 0. Floor(-2.1) liefert -3. Floor(3) liefert unverändert 3.
IEEERemainder(x, y)
liefert den Rest einer ganzzahligen Division, also x Mod y; genau genommen wird zuerst Q=x/y berechnet, wobei Q dann zur nächsten ganzen Zahl auf- oder abgerundet wird; IEEERemainder liefert dann x-Q*y als Ergebnis. IEEERemainder(12, 5) liefert 2. IEEERemainder(-12, 5) liefert -2. IEEERemainder(12, -5) liefert 2. IEEERemainder(-12, -5) liefert -2. IEEERemainder(12.1, 5) liefert 2.1 = 12.1 - 2 * 5. IEEERemainder(12, 5.1) liefert 1.8 = 12 - 2 * 5.1.
Min(x ,y), Max(x, y)
liefert die kleinere bzw. größere von zwei Zahlen. Es ist nicht möglich, mehr als zwei Argumente zu übergeben.
Round(x, n)
rundet auf die angegebene Stellenanzahl: Round(3.14159, 3) liefert 3.142.
Die VB-Funktionen CByte, CShort, CInt und CLng sind eigentlich nicht zum Runden von Zahlen gedacht, sondern zur Umwandlung zwischen verschiedenen Datentypen (siehe auch Abschnitt 8.4). Dabei wird wie mit Round gerundet. Im Unterschied zu Round liefern die Funktionen aber keine Double-Zahlen als Ergebnis, sondern jeweils den entsprechenden Datentyp.
296
8 Zahlen, Zeichenketten, Datum und Uhrzeit
VB-Konvertierungsfunktionen CByte(x)
wandelt x in eine Byte-Zahl um und rundet dabei wie Round: CByte(2.5) liefert 2. CByte(2.51) liefert 3. CByte(3.49) liefert 3. CByte(3.5) liefert 4.
CShort(x)
wie oben, liefert aber eine Short-Zahl.
CInt(x)
wie oben, liefert aber eine Integer-Zahl.
CLng(x)
wie oben, liefert aber eine Long-Zahl.
8.1.6
Zufallszahlen
TIPP
Grundsätzlich gibt es zwei Möglichkeiten, Zufallszahlen zu erzeugen: Entweder setzen Sie die aus VB6 vertrauten Funktionen ein (Microsoft.VisualBasic.VBMath), oder Sie verwenden die neuen .NET-Methoden (System.Random). Beide hier beschriebenen Methoden liefern keine echten Zufallszahlen, sondern nur Pseudozufallszahlen, die anhand relativ einfacher mathematischer Modelle erzeugt werden. Für fortgeschrittene Anwendungen sind diese Zahlen allerdings zu wenig zufällig. Bessere Zufallszahlen liefern die Methoden von System.Security.Crypthography. Die Anwendung dieser Methoden ist allerdings etwas komplizierter; außerdem dauert die Erzeugung von Zufallszahlen dann deutlich länger.
VB-Zufallszahlen Rnd liefert eine 8-stellige Zufallszahl (Single) zwischen 0 (inklusive) und 1 (exklusive). Um Zufallszahlen in einem bestimmten Bereich zu erhalten, müssen Sie mit Rnd weiterrech-
nen: a + Rnd * (b-a) Int(a + Rnd * (b-a+1))
'liefert Zufallszahlen zwischen 'a (inklusive) und b (exklusive) 'liefert ganze Zufallszahlen 'zwischen a (inkl.) und b (inkl.)
Wenn Sie vermeiden möchten, daß Ihr Visual-Basic-Programm nach jedem Start die gleiche Abfolge von Zufallszahlen generiert, dann müssen Sie zum Programmstart das Kommando Randomize ausführen (entweder ohne Parameter oder mit einem pseudo-zufälligen Parameter, der sich etwa aus der Uhrzeit und dem Datum ergibt).
8.1 Zahlen
297
Microsoft.VisualBasic.VBMath-Methoden Rnd()
liefert eine Single-Zufallszahl.
Rnd(x)
liefert für x=0 nochmals dieselbe Zufallszahl, für x0 die nächste Zufallszahl (wie Rnd()).
Randomize
initialisiert den Zufallszahlengenerator mit einem zufälligen Startwert.
Randomize(x)
initialisiert den Zufallszahlengenerator mit dem Startwert x.
.NET-Zufallszahlen Die mscorlib-Bibliothek (Datei mscorlib.dll) stellt in der Klasse System.Random einige Methoden zur Erzeugung von Zufallszahlen zur Verfügung. Damit Sie diese Methoden verwenden können, müssen Sie vorher eine Objekt des Typs Random erzeugen. Die in der folgenden Tabelle genannten Methoden müssen auf dieses Objekt angewandt werden. Random produziert bei jedem Programmstart andere Zufallszahlen. Aus diesem Grund ist eine mit Randomize vergleichbare Methode nicht erforderlich. (Wenn Sie immer wieder dieselben reproduzierbaren Zufallszahlen verwenden möchten – etwa um eine Programmfunktion zu testen –, müssen Sie mit den oben beschriebenen VB-Funktionen arbeiten.)
System.Random-Methoden Next()
liefert eine Integer-Zufallszahl zwischen 0 (inklusive) und 2147483647 (exklusive).
Next(n)
liefert eine Integer-Zufallszahl zwischen 0 (inklusive) und n (exklusive). n muss selbst eine Integer-Zahl größer 0 sein. Next(4) liefert also Zufallszahlen zwischen 0 und 3.
Next(n1, n2)
liefert eine Integer-Zufallszahl zwischen n1 (inklusive) und n2 (exklusive). Next(7, 12) liefert also Zufallszahlen zwischen 7 und 11.
NextBytes(bytearray())
füllt das Byte-Feld mit Zufallsdaten.
NextDouble()
liefert eine Double-Zufallszahl zwischen 0 (inklusive) und 1 (exklusive).
Das folgende Miniprogramm schreibt zehn Zufallszahlen zwischen 1 und 100 (jeweils inklusive) in ein Konsolenfenster.
298
8 Zahlen, Zeichenketten, Datum und Uhrzeit
Module Module1 Sub Main() Dim i As Integer Dim myrand As New Random() For i = 1 To 10 Console.WriteLine(myrand.Next(1, 101)) Next Console.WriteLine("Drücken Sie eine Taste.") Console.ReadLine() End Sub End Module
8.2
Zeichenketten
8.2.1
Grundlagen
VB.NET kennt zwei Datentypen zum Umgang mit Zeichenketten: In Char-Variablen kann ein einzelnes Zeichen und in String-Variablen können Zeichenketten beinahe beliebiger 31 Länge (bis zu 2 Zeichen) gespeichert werden. In beiden Fällen verwendet VB.NET intern Unicode zur Codierung der Zeichen (zwei Byte pro Zeichen).
Notation Zeichenketten werden zwischen zwei Hochkommas eingeschlossen. Wenn Sie das Zeichen " selbst in einer Zeichenkette speichern möchten, wird die Zuweisung ein wenig unübersichtlich: Sie müssen " verdoppeln: Wenn Sie s1 = """abc""efg""" ausführen, enthält s1 anschließend die Zeichenkette "abc"efd". Dim s = s = s =
s As String "abc" "a""bc" """abc"""
'Zeichenkette abc 'Zeichenkette a"bc 'Zeichenkette "abc"
Konvertierung zwischen Char und String Die Umwandlung von Char zu String ist immer problemlos. Den umgekehrten Fall, also etwa die Zuweisung charvariable = stringvariable, akzeptiert der VB.NET-Kompiler dagegen nur bei Option Strict Off. Wenn Sie mit Option Strict On arbeiten, müssen Sie CChar einsetzen. Die folgenden Zeilen geben einige Beispiele, wie die Zuweisung korrekt durchgeführt werden kann. (Die letzte Variante ist am langsamsten.)
8.2 Zeichenketten
Dim c = c = c = c =
299
c As Char, s As String CChar(s) s.Chars(0) GetChar(s, 1) Convert.ToChar(Left(s, 1))
Initialisierung String-Variablen können direkt bei der Deklaration initialisiert werden. Die folgenden Beispiele zeigen einige Syntaxvarianten: Dim Dim Dim Dim Dim Dim
s1 s2 s3 s4 s5 s6
As As As As As As
String = "abc" String = Space(5) New String("a"c, 10) String = StrDup(3, "x") String = LSet("abc", 10) String = RSet("abc", 10)
's1 's2 's3 's4 's5 's6
= = = = = =
"abc" " " "aaaaaaaaaa" "xxx" "abc " " abc"
Space liefert einfach die angegebene Anzahl von Leerzeichen zurück. New String(c, n) liefert ein Zeichenkette, die n Mal das im ersten Parameter angegebene Zeichen enthält. Als Parameter muss ein Char-Zeichen übergeben werden, d.h., es ist nicht möglich, eine Zeichenkette zu vervielfältigen. (Wenn Sie mit Option Strict On arbeiten, müssen Sie den Parameter wie im obigen Beispiel mit dem Literal c als Char-Zeichen kennzeichnen!) StrDup hat dieselbe Funktion wie NewString, akzeptiert aber auch eine normale Zeichen-
kette (von der aber nur das erste Zeichen berücksichtigt wird!). Beachten Sie, dass die Parameterreihenfolge gegenüber New String vertauscht ist. LSet und RSet kopiert eine Zeichenkette in eine Variable und fügt dann so viele Leerzeichen am Ende bzw. am Beginn der neuen Zeichenkette ein, dass diese eine vorgegebene Länge erreicht. Besonders praktisch ist das bei Zahlen, die rechtsbündig in Zeichenketten gespeichert werden sollen (oder müssen): Module Module1 Sub Main() Dim i As Integer, s(9) As String For i = 0 To 9 s(i) = RSet((5 ^ i).ToString, 10) Console.WriteLine("s(" & i & ")=" & s(i)) Next End Sub End Module
Das Programm demonstriert gleichzeitig die wichtige Methode ToString, die in VB.NET auf beinahe jedes Objekt angewandt werden kann – auch auf geklammerte arithmetische oder logische Ausdrücke. Das Programm liefert folgendes Ergebnis:
300
8 Zahlen, Zeichenketten, Datum und Uhrzeit
s(0)= s(1)= s(2)= s(3)= s(4)= s(5)= s(6)= s(7)= s(8)= s(9)=
1 5 25 125 625 3125 15625 78125 390625 1953125
Die Verkettungsoperatoren + und & Mehrere Zeichenketten können mit + zusammengesetzt werden. "ab"+"cd" liefert also "abcd". Noch universeller ist der Operator &, der Daten in anderen Typen (Zahlen, Datum und Uhrzeit) automatisch in Zeichenketten umwandelt. "ab" & 1/3 ergibt damit "ab0,333333333333333". Wie bei anderen Datentypen sind auch bei Zeichenketten die Operatoren =+ und =& zulässig, um einer String-Variablen eine Zeichenkette hinzuzufügen:
VORSICHT
Dim s As String = "abc" s += "efg" 's enthält jetzt "abcefg" s &= 1 / 2 's enthält jetzt "abcefg0,5"
Bei der Umwandlung von Zahlen, Daten und Zeiten in Zeichenketten durch & oder &= wird automatisch die gültige Landeseinstellung berücksichtigt. Das Programm verhält sich daher unterschiedlich, je nachdem, wo es ausgeführt wird. Wenn Sie das nicht möchten, müssen Sie die Konvertierung explizit mit Funktionen wie Str durchführen. (Mehr Informationen zum Thema Konvertierung gibt Abschnitt 8.4.)
Vordefinierte Zeichenketten (Konstanten) Einige oft benötigte Zeichenketten sind als Konstanten vordefiniert – und das gleich doppelt: einmal in Microsoft.VisualBasic.Constants und ein zweites Mal in Microsoft.VisualBasic.ControlChars. Die Konstanten sind dann praktisch, wenn mehrzeilige Zeichenketten (etwa für das Textfeld) oder Tabellen gebildet werden. Ob Sie lieber die aus VB6 vertrauten vbXxx-Konstanten verwenden oder die (mit etwas mehr Tippaufwand verbundenen) neuen ControlChars-Konstanten, ist eine reine Geschmacksfrage.
8.2 Zeichenketten
301
.Constants
.ControlChars
Inhalt
Verwendung
vbBack
ControlChars.Back
Chr(8)
Backspace-Zeichen
vbCr
ControlChars.Cr
Chr(13)
Wagenrücklauf (Carriage Return)
vbCrLf
ControlChars.CrLf
Chr(13)+Chr(10)
Zeilenumbruch unter Windows
vbFormFeed
ControlChars.FormFeed
Chr(12)
neue Seite
vbLF
ControlChars.LF
Chr(10)
neue Zeile (Line Feed)
vbNewLine
ControlChars.NewLine
Chr(13)+Chr(10)
unter Windows wie ControlChars.CrLf
vbTab
ControlChars.NullChar
Chr(0)
Zeichen mit dem Code 0
ControlChars.Quote
Chr(34)
Anführungszeichen "
ControlChars.Tab
Chr(9)
Tabulator
vbVerticalTab ControlChars.VerticalTab Chr(11)
vertikaler Tabulator
Alle Konstanten, die nur ein Zeichen enthalten, sind Char-Konstanten. Lediglich bei [vb]CrLf und [vb]NewLine handelt es sich um String-Konstanten. [vb]NewLine enthalten die Codes zur Markierung einer neuen Zeile. Die Zeichenkette hat je nach Rechner einen unterschiedlichen Wert (vbCrLf unter Windows) und wird dann interessant, falls Visual Basic einmal auch für andere Betriebssysteme zur Verfügung stehen sollte. (Dieselbe Information kann auch mit Environment.NewLine ermittelt werden.)
8.2.2
Methoden zur Bearbeitung von Zeichenketten
Eigenschaften und Methoden zur Bearbeitung von Zeichenketten gibt es wie Sand am Meer. •
Zum einen stehen alle aus VB6 bekannten Methoden (Left, Mid etc.) weiterhin per Default zur Verfügung (Klasse Microsoft.VisualBasic.Strings).
•
Zum anderen enthält die .NET-Bibliothek mscorlib.dll zahllose neue Eigenschaften und Methoden, die auf Char- und String-Variablen angewandt werden können. Diese Schlüsselwörter sind Klassenmitglieder von System.String bzw. System. Char.
Die am häufigsten eingesetzten Methoden werden auf den folgenden Seiten vorgestellt. Einen weitgehenden Überblick geben die Syntaxtabellen am Ende dieses Abschnitts. (Einige Methoden, deren Anwendung unter VB.NET selten sinnvoll sind, werden aus Platzgründen nicht beschrieben.)
302
8 Zahlen, Zeichenketten, Datum und Uhrzeit
VORSICHT
Es gibt zwei große Unterschiede zwischen den beiden Gruppen: • Der Startindex zum Zugriff auf Zeichen ist unterschiedlich: Herkömmliche Zeichenkettenmethoden verarbeiten das erste Zeichen einer Zeichenkette mit dem Index eins: Left(s, 1) liefert also das erste Zeichen der String-Variablen s. Die neuen .NET-Methoden verwenden dagegen den Index null: s.Chars(1) liefert das zweite Zeichen! • Nicht initialisierte Zeichenketten werden unterschiedlich interpretiert: Herkömmliche Zeichenkettenmethoden betrachten nicht initialisierte Zeichenketten so, als enthielten sie "". Len(s) liefert in einem derartigen Fall einfach 0. Die neuen .NET-Methoden (z.B. s.Length) verursachen in diesem Fall dagegen einen Fehler.
Herkömmliche Methoden zur Bearbeitung von Zeichenketten Die drei wichtigsten Methoden sind Left, Mid und Right: Left(s,n) ermittelt die n ersten Zeichen, Right(s,n) die n letzten Zeichen der Zeichenkette. Mid(s,n) liefert alle Zeichen ab dem n-ten Zeichen, Mid(s,n,m) liefert ab dem n-ten Zeichen m Zeichen. Mid kann auch als Befehl verwendet werden, um einige Zeichen einer Zeichenkette zu verändern. In allen Methoden wird das erste Zeichen mit n=1 angesprochen (nicht n=0).
TIPP
Dim s As String s = "abcdef" Mid(s, 3)="12"
's enthält jetzt "ab12e"
In Windows-Programmen müssen Sie statt Left und Right die etwas umständlichere Schreibweise Strings.Left bzw. Strings.Right verwenden, um einen Konflikt mit den Fenstereigenschaften Left und Right zu vermeiden.
Len ermittelt die Anzahl der Zeichen einer Zeichenkette. (Das Ergebnis von Len ist nicht die Anzahl der Bytes!) Len kann auch für alle anderen elementaren VB-Variablentypen verwendet werden und liefert in den meisten Fällen die Anzahl der Bytes, die zur Speicherung der eigentlichen Daten benötigt werden. (Intern benötigt VB aber unter Umständen deutlich mehr Speicher, wie bereits in Abschnitt 4.6.3 ausgeführt worden ist.) UCase wandelt alle Buchstaben in Großbuchstaben um, LCase liefert Kleinbuchstaben. Trim eliminiert die Leerzeichen am Anfang und Ende der Zeichenkette, LTrim und RTrim arbeiten nur auf jeweils einer Seite. (Genau genommen entfernen die Trim-Methoden nicht nur Leerzeichen, sondern so genannten white space. Dazu zählen auch Tabulator-, Zeilen-
trennzeichen und eine Reihe anderer Sonderzeichen. Die vollständige Liste finden Sie in der Online-Dokumentation, wenn Sie im Index nach white space suchen.) Zum Suchen einer Zeichenkette in einer anderen steht die Methode InStr zur Verfügung. Die Methode ermittelt die Position, an der die gesuchte Zeichenkette zum ersten Mal gefunden wird. InStr("abcde", "cd") liefert beispielsweise 3. Wenn die Suche erfolglos bleibt, gibt die Methode den Wert 0 zurück. Optional kann in einem Parameter angegeben wer-
8.2 Zeichenketten
303
den, an welcher Position die Suche begonnen wird. InStr berücksichtigt Option Compare (siehe Abschnitt 8.2.3), sofern nicht durch einen weiteren optionalen Parameter das gewünschte Vergleichsverhalten vorgegeben wird. InstrRev funktioniert wie Instr, durchsucht die Zeichenkette aber von hinten. Beispielsweise liefert InstrRev("abcababc","ab") den Wert 6. StrReverse dreht eine Zeichenkette einfach um (das erste Zeichen wird zum letzten). s = StrReverse("abcde")
'liefert "edcba"
Split zerlegt eine Zeichenkette in ein String-Feld. Dabei kann im zweiten Parameter ein beliebiges Trennzeichen angegeben werden. (Per Default wird " " verwendet.) Mit einem weiteren Parameter können Sie die Anzahl der Elemente limitieren. Dim a As String, b() As String, c As String a = "abc efg" b = Split(a) 'liefert b(0)="abc", b(1)="efg"
Die Umkehrmethode zu Split lautet Join und setzt die einzelnen Zeichenketten wieder zusammen. c = Join(b)
'liefert c="abc efg"
Eine Hilfe bei der Verarbeitung des aus Split resultierenden Felds bietet Filter: Die Methode erwartet im ersten Parameter ein eindimensionales Feld mit Zeichenketten und im zweiten Parameter eine Suchzeichenkette. Das Ergebnis ist ein neues Feld mit allen Zeichenketten, in denen die Suchzeichenkette gefunden wurde. Die zulässigen Indizes des Ergebnisfelds können mit UBound und LBound ermittelt werden. Dim x(), y() As String x = Split("abc:ebg:hij", ":") y = Filter(x, "b") 'liefert y(0)="abc", y(1)="ebg" Replace ersetzt in einer Zeichenkette einen Suchausdruck durch einen anderen Ausdruck. Komplexe Suchmuster werden zwar nicht unterstützt, aber für einfache Anwendungen reicht Replace aus. Im folgenden Beispiel werden Kommas durch Punkte ersetzt. s = Replace("12,3 17,5 18,3", ",", ".") Str und Format wandeln Zahlen in Zeichenketten um. Val liefert den Wert einer
VERWEIS
Zahl. Diese und andere Umwandlungsmethoden werden im nächsten Abschnitt ausführlicher beschrieben. Einfache Kommandos zur Ein- und Ausgabe von Zeichenketten sind beispielsweise MsgBox, Windows.Forms.MessageBox und InputBox: MsgBox bzw. die neue Methode MessageBox zeigen die angegebene Zeichenkette in einer Dialogbox an, die mit OK quittiert werden kann. InputBox ermöglicht die Eingabe von Zeichenketten, wobei ein beschreibender Text und eine Defaulteingabe als Parameter übergeben werden können. Alle drei Kommandos werden in Abschnitt 15.5 vorgestellt.
304
8 Zahlen, Zeichenketten, Datum und Uhrzeit
.NET-Methoden zur Bearbeitung von Zeichenketten Unter den .NET-Methoden (System.String.*) befinden sich einige recht praktische Hilfen, um eine Zeichenkette in einer anderen zu finden. So testet etwa s.EndsWith("efgh"), ob s mit den vier angegebenen Buchstaben endet. Analog überprüft s.StartsWith("abc"), ob s mit den drei Buchstaben "abc" endet. An s.IndexOfAny wird ein Char-Feld übergeben. Die Methode sucht nun nach der ersten Stelle in s, das mit einem beliebigen Zeichen aus dem Feld übereinstimmt. Wenn keines der Zeichen gefunden werden kann, liefert die Methode -1 zurück. (Die Variante LastIndexOfAny funktioniert ebenso, beginnt die Suche aber von hinten. An beide Methoden kann der Suchbereich durch zwei weitere, optionale Parameter eingeschränkt werden.) Dim Dim Dim n =
s As String = "ab,cd.ef:g" c() As Char = {"."c, ","c, ":"c} n As Integer s.IndexOfAny(c) 'n enthält 2
Auch zur Bearbeitung von Zeichenketten wartet .NET mit einigen Methoden auf, die VB bisher fehlten: Ausgesprochen praktisch ist etwa Insert, um eine Zeichenkette in eine andere an einer beliebigen Position einzufügen. Als erster Parameter wird die Position angegeben, an der mit dem Einfügen begonnen wird. Dim s1 As String = "abcdef", s2 As String, s3 As String s2 = s1.Insert(2, "XYZ") 's2 enthält "abXYZcdef" Remove entfernt einige Zeichen aus einer der Zeichenketten: s3 = s2.Remove(5, 2)
's3 enthält "abXYZef"
Manchmal sollen Zeichenketten anhand eines einfachen Zahlenwerts identifiziert werden – etwa wenn die Zeichenkette als Schlüssel für einen raschen Zugriff in einer Aufzählung oder in einer Datenbank dienen soll. Die Methode GetHashCode ist dabei eine große Hilfe. Sie liefert einen Integer-Wert, der aus dem Inhalt der gesamten Zeichenkette berechnet wird.
HINWEIS
Beachten Sie bitte, dass der hash-Wert nicht eindeutig ist! Es kann vorkommen, dass zwei unterschiedliche Zeichenketten zufällig denselben hash-Wert liefern. (Es wäre ja ein Wunder, wenn in einer 32-Bit-Zahl der gesamte Inhalt einer beliebig langen Zeichenkette ausgedrückt werden könnte – und Wunder sind in der Informatik selten.) GetHashCode liefert für eine bestimmte Zeichenkette immer wieder denselben hashWert. Mit anderen Worten: wenn zwei String-Variablen denselben Inhalt haben, ist auch ihr hash-Wert derselbe. Nur die umgekehrte Schlussfolgerung ist nicht zuläs-
sig. Interessante Hintergrundinformationen zu GetHashCode finden Sie auch bei der Dokumentation zu System.Object.GetHashCode: ms-help://MS.VSCC/MS.MSDNVS.1031/cpref/html/frlrfSystemObjectClassGetHashCodeTopic.htm
VERWEIS
8.2 Zeichenketten
305
Für fast jedes .NET-Objekt steht die Methode ToString zur Verfügung. Damit kann der Inhalt des Objekts (oder zumindest eine Beschreibung, und sei es nur über den Datentyp) in Form einer Zeichenkette ausgedrückt werden. Selbst auf beliebige Ausdrücke können Sie ToString anwenden: (a+3>5).ToString liefert "True" oder "False" (je nach Inhalt von a). (1/3).ToString liefert im deutschen Sprachraum "0,333333333333333".
Weitere Informationen zu ToString finden Sie im Zusammenhang mit den anderen Konvertierungsmethoden in Abschnitt 8.4.
.NET-Methoden zur Bearbeitung von einzelnen Zeichen Auch für den Datentyp System.Char gibt es zahllosen Methoden. Da man mit einem einzelnen Zeichen nicht so viel anstellen kann wie mit einer ganzen Zeichenkette, beschränken sich die meisten dieser Methoden darauf, Informationen über die Art des Zeichens zu geben. Beispielsweise liefert IsDigit(c) das Ergebnis True oder False, je nachdem, ob c eine Ziffer enthält oder nicht. Die wichtigsten IsXxx-Methoden sind in der Syntaxzusammenfassung am Ende dieses Abschnitts aufgezählt.
8.2.3
Vergleich von Zeichenketten
Zeichenketten können mit den Operaten =, < und > ohne Probleme verglichen werden. Der Vergleich wird allerdings binär durchgeführt (d.h., es wird die binäre Repräsentierung der Zeichenketten miteinander verglichen). Wenn Sie auf dieser Basis eines binären Vergleichs einen Sortieralgorithmus programmieren, gelten Großbuchstaben kleiner als Kleinbuchstaben, deutsche Sonderzeichen kleiner als alle ASCII-Zeichen etc. Zahlen werden entsprechen ihrer Zeichen (und nicht entsprechend ihres Werts) sortiert. Für einige Testzeichenketten gilt somit die folgende Ordnung: 100 < 27 < ABC < Abcd < Barenboim < Bär < Bären < abc < bar < bärtig < Ärger Wenn Sie am Begin der Codedatei die Option Compare Text einfügen, führt VB.NET Zeichenkettenvergleiche etwas intelligenter aus: Groß- und Kleinbuchstaben werden als gleichwertig betrachtet, deutsche Sonderzeichen werden mit den entsprechenden Buchstaben gleichgesetzt (A=Ä etc.). Es gilt nun die folgende Ordnung: 100 < 27 < ABC < Abcd < abc < Ärger < bar < Bär < Bären < Barenboim < bärtig
StrComp (VB-Methode) Beachten Sie, dass Option Compare Text alle Zeichenkettenvergleiche der gesamten Textdatei betrifft und diese spürbar verlangsamt! Wenn Sie die Groß- und Kleinschreibung sowie ausländische Zeichen nur bei einzelnen Vergleichen korrekt berücksichtigen möchten, sollten Sie die Methode StrComp einsetzen. Bei dieser Methode können Sie das gewünschte Vergleichsverfahren in einem optionalen dritten Parameter angegeben. StrComp liefert 0
306
8 Zahlen, Zeichenketten, Datum und Uhrzeit
zurück, wenn beide Zeichenketten gleich sind, -1, wenn die erste kleiner ist als die zweite, und 1, wenn die erste größer ist.
HINWEIS
ergebnis = StrComp(s1, s2) 'Vergleich je nach Option Compare ergebnis = StrComp(s1, s2, CompareMethod.Binary) ergebnis = StrComp(s1, s2, CompareMethod.Text)
Ein einfaches Beispielprogramm, das Sortierungsvarianten auf der Basis von StrComp und String.Compare demonstriert, finden Sie im Verzeichnis zahlen-zeichenketten\text-compare-sort. Beachten Sie bitte, dass es zum Sortieren von Feldern effizientere Verfahren gibt – siehe Abschnitt 9.3.2!
String.Compare (.NET-Methode) Ähnlich wie StrComp vergleicht die Methode String.Compare zwei Zeichenketten und liefert liefert 0, wenn beide Zeichenketten gleich sind, einen Wert kleiner 0, wenn die erste kleiner ist als die zweite, und einen Wert größer 0, wenn die erste größer ist. Beispielsweise liefert String.Compare("a", "b") das Ergebnis -1. Damit enden die Ähnlichkeiten zu StrComp aber. Großbuchstaben gelten für String.Compare größer als ihr entsprechender Kleinbuchstabe, aber kleiner als der nächste Buchstabe: Es gilt also "a" "abc 3 efg 7" s = String.Format("abc {1:d} efg {0:d}", 3, 7) '--> "abc 7 efg 3"
Je nach Format kann zusätzlich angegeben werden, wie viele Stellen zur Formatierung verwendet werden sollen. Die allgemeine Syntax lautet dann {n,m:code} (wobei m die Stellenanzahl ist). Auch dazu ein Beispiel (beachten Sie die Anzahl der Leerzeichen vor der Ziffer 3!): s = String.Format("abc {0,7:d} efg", 3)
'-->
"abc
3 efg"
Wenn die Stellenanzahl m negativ angegeben wird, werden die Leerzeichen nach der formatierten Zeichenkette (statt davor) eingefügt: s = String.Format("abc {0,-7:d} efg", 3)
'-->
"abc 3
efg"
TIPP
342
8 Zahlen, Zeichenketten, Datum und Uhrzeit
Sie können in der Formatierungszeichenkette auch ganz auf eine Formatangabe verzichten. In diesem Fall werden die Daten einfach mit ToString ohne besondere Formatoptionen (also mit dem ToString-Default) in Zeichenketten umgewandelt. Für einfache Ausgaben ist diese Kurzschreibweise meistens ausreichend. Dazu noch ein Beispiel: String.Format("abc {0} efg", 3) liefert "abc 3 efg".
Falls Sie zusätzliche (landesspezifische) Formatierungsoptionen verwenden möchten, können Sie ein IFormatProvider-Objekt als ersten Paramter an String.Format übergeben, also beispielsweise String.Format(cult, "{0:D}", Now). Neben String.Format gibt es unzählige weitere .NET-Methoden, die dieselben Parameter verarbeiten – beispielsweise Console.Write[Line] oder IO.TextWriter. Write[Line]. Es ist also selten erforderlich, zuerst mit String.Format eine entsprechende Zeichenkette zu bilden und diese dann an eine andere Methode zu übergeben; oft kann die Formatierung direkt mit der Ausgabemethode durchgeführt werden. Das verhilft zu einem kompakteren Code. Die beiden folgenden Zeilen Console.WriteLine("{0:D}", Now) Console.WriteLine("abc {0:d} efg {1:d}", 3, 7)
führen zu dieser Ausgabe im Konsolenfenster: Dienstag, 31. Dezember 2002 abc 3 efg 7
Beachten Sie aber, dass diese Format-Schreibweise leider nicht für die Write[Line]-Methoden der Debug-Klasse verwendet werden kann. .NET-Formatierungsmethoden obj.ToString
verwendet das Defaultformat ("") und die Sprache entsprechend der Systemeinstellungen.
obj.ToString("format")
verwendet zur Formatierung das angegebene Format.
obj.ToString("format", iform)
wie oben, berücksichtigt zusätzliche Formatoptionen. (Zulässige IFormatProvider-Parameter sind CultureInfo-, DateTimeFormatInfo- und NumberFormatInfo-Objekte.)
String.Format("format", obj1, obj2, obj3 ...)
verwendet zur Formatierung das angegebene Format. Der Formatcode muss in der Schreibweise {n} oder {n:code} oder {n,m:code} angegeben werden. Dabei gibt n die Parameternummer an (beginnend mit 0), m die maximale Stellenanzahl der Zeichenkette und code die gewünschte Formatierung.
String.Format(iform, "format", obj1, obj2 ...)
wie oben, berücksichtigt aber zusätzliche Formatoptionen (z.B. Ländereinstellungen).
8.5 .NET-Formatierungsmethoden
8.5.2
343
Zahlen formatieren
Dieser Abschnitt stellt eine Menge Codes zur Formatierung von Zahlen vor. Diese Codes können folgendermaßen angewendet werden (hier für den Code e): Dim s As String, x As Double = Math.PI s = x.ToString("e3") 's = "3,142e+000" s = String.Format("{0:e3}", x) 's = "3,142e+000"
Die Formate c bis p können für alle numerischen Datentypen verwendet werden. Bei einigen Formaten kann durch eine nachgestellte Zahl die Anzahl der Stellen hinter dem Komma angegeben werden. Per Default werden im Regelfall zwei Nachkommastellen verwendet. Die folgende Tabelle zeigt, wie die Double-Zahlen 123456789 und -0,0000123 formatiert werden (bei deutscher Ländereinstellung und mit € als Währungssymbol). Vordefinierte Formatcodes zur Formatierung von Zahlen (Methode String.Format) c
123.456.789,00 € / -0,00 €
Währungsformat (currency)
c3
123.456.789,000 € / -0,000 €
Währungsformat mit drei Nachkommastellen
e
1,234568e+008 / -1,230000e-006
wissenschaftliches Format (exponential)
e3
1,235e+008 / 1,230e-005
wissenschaftliches Format mit drei Nachkommastellen
E
1,234568E+008 / -1,230000E-006
wie oben, aber Exponentialschreibweise mit E statt mit e
f
123456789,00 / -0,00
Festkommaformat (fixed-point)
f3
123456789,000 / -0,000
Festkommaformat mit drei Nachkommastellen
g
123456789 / -1,23e-06
allgemeines Format, möglichst kompakte Darstellung von Zahlen (general)
n
123.456.789,00 / -0,00
Format mit Tausendertrennung (number)
n3
123.456.789,000 / -0,000
Format mit Tausendertrennung mit drei Nachkommastellen
p
12,345,678,900,00% / -0,00%
Prozentzahlen (Achtung: der Wert 1 wird als 100 % dargestellt!)
p3
12,345,678,900,00% / -0,001%
Prozentzahlen mit drei Nachkommastellen
r
123456789 / -1,23E-06
Format, das ein verlustfreies Wiedereinlesen (z.B. mit CDbl) der Daten ermöglicht (roundtrip). Allerdings ist auch dieses Format von der Landeseinstellung abhängig und daher für internationale Anwendungen ungeeignet! Das Format ist ausschließlich für Single- und Double-Zahlen gedacht.
344
8 Zahlen, Zeichenketten, Datum und Uhrzeit
Die Formate d und x können ausschließlich für Integerzahlenformate verwendet werden (nicht aber für Single, Double und Decimal!). Bei beiden Formaten kann die gewünschte Stellenanzahl angegeben werden – in diesem Fall werden entsprechend viele Nullen eingefügt. Die folgende Tabelle zeigt, wie die Integerzahlen i1=123456789 und i2=-123 formatiert werden. Vordefinierte Formatcodes für Integerzahlen (Byte, Short, Integer, Long) d
123456789 / -123
dezimales Format
d5
123456789 / -00123
dezimales Format mit fünf Stellen
x
75bcd15 / ffffff85
hexadezimales Format
Einzelcodes zur individuellen Formatierung von Zahlen Wenn Ihnen die vordefinierten Formate nicht ausreichen, können Sie die Formatzeichenkette auch ganz selbst bilden. .NET sieht dazu eine Menge Codes vor, die sowohl für Fließkomma- als auch für Integerzahlen verwendet werden dürfen. Einzelcodes zur Formatierung von Zahlen (Methode String.Format) 0
Platzhalter für eine Zahl bzw. für eine Stelle; nichtsignifikante Nullen werden durch das Zeichen 0 dargestellt
#
Platzhalter für eine Zahl bzw. für eine Stelle; nichtsignifikante Nullen werden durch Leerzeichen ersetzt
.
Dezimalpunkt (das tatsächlich verwendete Zeichen hängt von der Landeseinstellung ab!)
,
Tausendertrennung (wie oben); das Zeichen bewirkt gleichzeitig eine Division durch 1000, so dass die Zahl 1234567890 mit dem Formatcode "#,," zu "1235" wird.
%
Prozentzeichen (wie oben); das Zeichen bewirkt gleichzeitig eine Multiplikation mit 100, so dass die Zahl 0,753 mit dem Formatcode "0%" zu "75%" wird.
E+0 e+0
Exponentialdarstellung mit den Zeichen E oder e; das Vorzeichen des Exponenten wird immer angezeigt (1E+3 oder 1E-3); die Anzahl der 0en bestimmt die Stellenanzahl des Exponenten (d.h., das Format e+000 führt zu "1e+003")
E-0 e-0
wie oben, ein positives Vorzeichen des Exponenten wird aber nicht angezeigt (1E3 oder 1E-3).
fp;fn;f0
ermöglicht die Angabe von drei unterschiedlichen Formaten für positive Zahlen, negative Zahlen und für 0; Achtung, bei negativen Zahlen wird das Vorzeichen entfernt (d.h., die Zahl -1 wird durch das Format 0;0 als 1 dargestellt)!
8.5 .NET-Formatierungsmethoden
345
Die folgenden Zeilen liefern einige Beispiele für die Anwendung der obigen Codes bei deutscher Landeseinstellung. Um den Code möglichst kompakt zu halten, wurde die unübliche (aber durchaus zulässige) Schreibweise zahl.ToString gewählt. Dim s = s = s =
s As String 123.456.ToString("0") 123.456.ToString("0.00") 123.456.ToString("0000.0000")
's = "123" 's = "123,46" 's = "0123,4560"
s s s s
123.456.ToString("#") 0.0123.ToString("#") 0.0123.ToString("#.###") 0.0123.ToString("0.###")
's 's 's 's
= = = =
= = = =
"123" "" ",012" "0,012"
s = 1234.ToString("#,#,#") s = 123456789.ToString("#,#,#") s = 1234.ToString("0,000,000")
's = "1.234" 's = "123.456.789" 's = "0.001.234"
s = 0.0123.ToString("0%") s = 0.0123.ToString("0.00%")
's = "1%" 's = "1,23%"
s = 123456789.ToString("0e+00") s = 123456789.ToString("000e+00") s = 123456789.ToString("0000E-00")
's = "1e+08" 's = "123e+06" 's = "1235E05"
s =-1.ToString("0;(0);0")
's = "(1)"
Noch eine kurze Erklärung zum letzten Beispiel: Damit wird erreicht, das positive Zahlen und 0 normal dargestellt werden, negative Zahlen dagegen in Klammern und ohne Vorzeichen, wie dies bisweilen im englischen Sprachraum üblich ist.
Text und Sonderzeichen in der Formatzeichenkette In der Formatzeichenkette dürfen auch beliebige andere Zeichen enthalten sein. Diese werden einfach unverändert angezeigt. s = 123.ToString("abc#efg")
's = "abc123efg"
Etwas komplizierter wird es, wenn Sie Formatierungszeichen als Text anzeigen möchten (z.B. das Zeichen #). Dazu gibt es zwei Schreibweisen: Sie können dem betreffenden Zeichen das Zeichen \ voranstellen oder Sie können die Sonderzeichen zwischen Apostrophe stellen. Auch hierfür zwei Beispiele: s = 123.ToString("#") s = 123.ToString("'###' # '###'")
's = "#123" 's = "### 123 ###"
Landesunabhängige Formatierung von Zahlen Unabhängig davon, welche Formatcodes Sie verwenden, wird bei der Formatierung von Zahlen immer auch die Landeseinstellung berücksichtigt (z.B. bei der Auswahl der Zeichen zur Dezimal- und zur Tausendertrennung). Wenn Sie das vermeiden möchten, über-
346
8 Zahlen, Zeichenketten, Datum und Uhrzeit
geben Sie an die Formatmethode ein zusätzliches CultureInfo-Objekt mit den InvariantCulture-Einstellungen. Dim s As String, x As Double = 1/7 Dim cult As Globalization.CultureInfo = _ Globalization.CultureInfo.InvariantCulture s = x.ToString("r", cult) 's = "0.14285714285714285" s = String.Format(cult, "{0:r}", x) 's = "0.14285714285714285"
Dezimaltrennzeichen und Tausendertrennzeichen feststellen Wenn Sie wissen möchten, welche Zeichen am aktuellen Computer zur Dezimal- bzw. zur Tausendertrennung eingestellt sind, können Sie den folgenden Code zu Hilfe nehmen. Dim decimalpoint, groupseparator As Char decimalpoint = String.Format("{0:0.0}", 0).Chars(1) groupseparator = String.Format("{0:0,000}", 1000).Chars(1)
Formatcodes ausprobieren Um die verschiedenen Formatcodes rasch auszuprobieren, können Sie ein Programm nach dem folgenden Muster verwenden. Sub Main() Dim x1 As Double = 123456789 Dim x2 As Double = -0.00000123 Dim i As Integer, formstr As String Dim myformat() As String = {"c", "e", "f", "g", "n", "p", "r"} For i = 0 To myformat.Length - 1 formstr = myformat(i) + ": {0:" + myformat(i) + "} / {1:" + _ myformat(i) + "}" Console.WriteLine(formstr, x1, x2) Next i End Sub
Mehr Flexibilität bietet ein kleines Windows-Programm, in dem Sie in zwei Textfeldern eine Fließkommazahl und eine Formatzeichenkette eingeben können. Das Programm zeigt bei jeder Änderung die resultierende formatierte Zeichenkette sofort an (siehe Abbildung 8.4). Sie finden das Programm auf der beiliegenden CD (Verzeichnis zahlen-zeichenketten\ formattest). Auf einen Abdruck des gesamten Codes wird hier verzichtet. Die entscheidenden Prozedur, die die beiden Eingabefelder auswertet und die Ergebniszeichenkette ermittelt, sieht folgendermaßen aus: ' in zahlen-zeichenketten\formattest\form1.vb Private Sub UpdateResultLabel() Dim x As Double
8.5 .NET-Formatierungsmethoden
347
Try 'Fehlerabsicherung, siehe Kapitel 11 x = CDbl(txtNumber.Text) lblResult.Text = _ String.Format("{0:" + Trim(txtFormat.Text) + "}", x) Catch lblResult.Text = "error" End Try End Sub
Abbildung 8.4: Programm zum Testen von Formatcodes
8.5.3
Daten und Zeiten formatieren
Die folgende Tabelle zählt die vordefinierten Codes zur Formatierung von Datum und Zeit auf (zusammen mit Beispielen, die für die deutsche Ländereinstellung gelten). Die Codes können folgendermaßen angewendet werden (hier für den Code F): s s s s
= = = =
Now.ToString("F") String.Format("{0:F}", Now) Now.ToString("HH:mm:ss") String.Format("{0:HH:mm:ss}", Now)
Vordefinierte Formatcodes für Datum und Uhrzeit (Methode String.Format) d
03.12.2001
Datum kurz (date)
D
Montag, 3. Dezember 2001
Datum lang
f
Montag, 3. Dezember 2001 21:42
Datum lang plus Zeit (full)
F
Montag, 3. Dezember 2001 21:42:06
Datum lang plus Zeit lang
g
03.12.2001 21:42
Datum kurz plus Zeit kurz (general)
G
03.12.2001 21:42:06
Datum kurz plus Zeit lang
m
03 Dezember
Tag plus Monat (month)
r
Mon, 03 Dec 2001 21:42:06 GMT
Datum gemäß RFC1123 (Internet-Standard)
s
2001-12-03T21:42:06
Datum gemä ISO-8601-Standard (sortable)
348
8 Zahlen, Zeichenketten, Datum und Uhrzeit
Vordefinierte Formatcodes für Datum und Uhrzeit (Methode String.Format) t
21:42
Zeit kurz (time)
T
21:42:06
Zeit lang
u
2001-12-03 21:42:06Z
wie s, aber laut Dokumentation universal time statt lokaler Zeit
U
Montag, 3. Dezember 2001 20:42:06
Variante zu u
y
Dezember 2001
Monat und Jahr (year)
Einige der folgenden Zeichen repräsentieren für sich allein ein komplettes Datumsformat (siehe Tabelle oben). Erst in Kombination mit anderen Formatierungszeichen werden sie als Einzelcodes interpretiert. Einzelcodes für Uhrzeit (Methode String.Format) f bis fffffff
123
Sekundenbruchteile (ein bis sieben Stellen, also von Zehntelsekunden bis 100 ns)
ss
59
Sekunden (00-59)
m
59
Minute (0-59) ohne vorangestellte 0
mm
59
Minute (00-59) mit vorangestellter 0
h
12
Stunde (1-12) ohne vorangestellte 0
hh
12
Stunde (1-12) mit vorangestellter 0
H
23
Stunde (0-23) ohne vorangestellte 0
HH
23
Stunde (0-23) mit vorangestellter 0
t
A
A oder P entsprechend AM oder PM (nur wenn AM/PM in der Ländereinstellung vorgesehen ist)
tt
AM
AM oder PM (nur bei entsprechender Ländereinstellung)
z
+1
Zeitzone (relativ zu GMT) ohne vorangestellte 0
zz
+01
Zeitzone mit vorangestellter 0
zzz
+01:00
Zeitzone vierstellig
Einzelcodes für Datum (Methode String.Format) ddd
Mon
Abkürzung für Monatstag
dddd
Montag
Monatstag voll ausgeschrieben
d
31
Monatstag (1-31) ohne vorangestellte 0
dd
31
Monatstag (01-31) mit vorangestellter 0
8.6 VB-Formatierungsmethoden
349
Einzelcodes für Datum (Methode String.Format) M
12
Monat (1-12) ohne vorangestellte 0
MM
12
Monat (1-12) mit vorangestellter 0
MMM
Dez
Abkürzung für den Monatsnamen
MMMM Dezember
Monatsname voll ausgeschrieben
y
1
Jahr zweistellig ohne vorangestellte 0
yy
01
Jahr zweistellig mit vorangestellter 0
yyy
2001
Jahr vierstellig
gg
n. Chr.
Zeitperiode oder Zeitära (nur, wenn dies für den eingestellten Kalender vorgesehen ist!)
8.6
VB-Formatierungsmethoden
Die zentrale VB-Methode zur Formatierung von Zahlen und Daten heißt einfach Format (definiert in der Klasse Microsoft.VisualBasic.Strings). Die allgemeine Syntax dieser Funktion lautet: Format(ausdruck, "myformat"). Beachten Sie, dass die hier beschriebene Methode Strings.Format nicht kompatibel mit der Methode String.Format aus dem vorigen Abschnitt ist! Damit wird der Ausdruck o entsprechend den Formatangaben durch myformat in eine Zeichenkette umgewandelt. Wie die beiden folgenden Beispiele zeigen, kann als myformat entweder eine vordefinierte String-Konstante oder eine Zeichenkette mit Formatzeichen übergeben werden: s = Format(Now, "General Date") s = Format(Now, "dd-mm-yyyy")
'liefert "31.12.2002 15:15:34" 'liefert "31-12-2002"
Die Methoden FormatNumber, FormatCurrency, FormatPercent und FormatDateTime sind vereinfachte Varianten zu Format. Zwar sind die Formatierungsmöglichkeiten stark eingeschränkt, dafür ist die Steuerung aber einfacherer. Alle vier Format-Methoden berücksichtigen die Systemeinstellung (Landeseinstellung, Währungssymbol etc.). Es besteht keine Möglichkeit, die Formatierung landesunabhängig durchzuführen. (Für diesem Zweck müssen Sie die unten beschriebenen .NET-Methoden einsetzen.) VB-Formatierungsmethoden (Klasse Microsoft.VisualBasic.Strings) Format(obj, "format")
formatiert das angegebene Objekt entsprechend der Formatierungszeichenkette.
FormatNumber(x, ...)
formatiert eine Zahl entsprechend den Angaben durch die weiteren Parameter.
FormatCurrency(x, ...)
formatiert eine Zahl als Geldbetrag.
350
8 Zahlen, Zeichenketten, Datum und Uhrzeit
VB-Formatierungsmethoden (Klasse Microsoft.VisualBasic.Strings) FormatPercent(x, ...)
formatiert eine Zahl als Prozentwert entsprechend den Angaben durch die weiteren Parameter.
FormatDateTime(d, format)
formatiert ein Datum oder eine Uhrzeit entsprechend dem (optionalen) Formatparameter.
8.6.1
Zahlen formatieren
FormatNumber FormatNumber formatiert die übergebene Zahl bei deutschen Systemeinstellungen per Default mit zwei Nachkommastellen und mit Punkten als Tausenderseparatoren. Daher liefert FormatNumber(123456.789) die Zeichenkette "123.456,79". Durch eine Reihe optionaler Parameter kann die Formatierung beeinflusst werden: s = FormatNumber(x, n, leading, parent, group)
•
x ist die zu formatierende Zahl (Datentyp Object, d.h. es dürfen Zahlen in allen Variab-
lentypen übergeben werden). •
n gibt die Anzahl der Nachkommastellen an (per Default 2).
•
leading gibt an, ob nicht signifikante Nullen dargestellt werden sollen (",23" oder "0,23"). Mögliche Einstellungen sind Tristate.True, Tristate.False oder Tristate.UseDefault. Per Default gilt UseDefault. Das bedeutet, dass das Verhalten durch die Systemeinstel-
lung gesteuert wird. Im deutschen Sprachraum werden nicht signifikante Nullen üblicherweise angezeigt. •
parent gibt an, ob negative Zahlen in Klammern gestellt werden sollen ("-1" oder "(1)"). Es gelten dieselben Einstellmöglichkeiten wie bei leading.
•
group gibt an, ob Tausenderseparatoren verwendet werden sollen. Abermals stehen dieselben Einstellmöglichkeiten wie bei leading zur Auswahl.
Einige Beispiele illustrieren die Anwendung von FormatNumber. s = FormatNumber(123456.789) s = FormatNumber(123456.789, 0) s = FormatNumber(123456.789, 0, , , TriState.False)
's = "123.456,79" 's = "123.457" 's = "123457"
FormatCurrency FormatCurrency funktioniert wie FormatNumber, fügt aber am Ende der Zeichenkette noch das durch die Systemeinstellung bestimmte Währungssymbol hinzu. FormatCurrency( 12345.678) liefert "12.345,68 €". Alle Parameter sind wie bei FormatNumber einzustellen.
8.6 VB-Formatierungsmethoden
351
FormatPercent FormatPercent formatiert die übergebene Zahl als Prozentwert. Das heißt, dass die Zahl mit Hundert multipliziert wird und mit zwei Nachkommastellen und einem Prozentzeichen dargestellt wird. FormatPercent(0.756) liefert im deutschen Sprachraum die Zeichenkette "75,60%". Die Methode kann mit denselben optionalen Parametern wie bei FormatNumber gesteuert werden. s = FormatPercent(x, n, leading, parent, group)
Noch einige Beispiele: s s s s
= = = =
FormatPercent(0.75678) FormatPercent(0.75678, 0) FormatPercent(0.0001234) FormatPercent(0.0001234, 4, TriState.False)
's 's 's 's
= = = =
"75,68%" "76%" "0,01%" ",0123%"
VB-spezifische Codes für Format Grundsätzlich können dieselben Kürzel wie bei den .NET-Formatierungsmethoden verwendet werden. Diese Kürzel werden hier nicht nochmals aufgezählt – Sie finden Sie in Abschnitt 8.5.2. Das folgende Beispiel zeigt die Anwendung des Formatcodes e für die Exponentialschreibweise. s = Format(123456, "e")
's = "1,23E+02"
Darüber hinaus kennt Format ein paar weitere Codes, deren Hauptvorteil darin besteht, dass ihre Bedeutung unmittelbar klar ist. (Dafür ist der Tippaufwand größer.) s = Format(123456, "Scientific")
's = "1,23E+02"
Die folgende Tabelle zeigt die Anwendung dieser Formate auf die Double-Zahlen 123456789 und -0,0000123. VB-spezifische Codes zur Formatierung von Zahlen (Methode Strings.Format) Standard
123.456.789,00 / -0,00
Format mit Tausendertrennung (number)
General Number
123456789 / -1,23e-06
allgemeines Format, möglichst kompakte Darstellung von Zahlen
Currency
123.456.789,00 € / -0,00 €
Währungsformat
Scientific
1,2E+08 / -1,23E-05
wissenschaftliches Format (exponential)
Fixed
123456789,00 / -0,00
Festkommaformat
Percent
12,345,678,900,00% / -0,00%
Prozentzahlen (Achtung: der Wert 1 wird als 100 % dargestellt!)
True/False
False / False
liefert False für Zahlen ungleich 0 und True für 0
Yes/No
Ja / Ja
liefert in der Landessprache Ja für Zahlen ungleich 0 und Nein für 0
352
8 Zahlen, Zeichenketten, Datum und Uhrzeit
VB-spezifische Codes zur Formatierung von Zahlen (Methode Strings.Format) On/Off
8.6.2
Ein / Aus
liefert in der Landessprache Ein für Zahlen ungleich 0 und Aus für 0
Daten und Zeiten formatieren
FormatDateTime FormatDateTime formatiert die Date-Variable per Default in der Form "31.12.2002 16:55:37". An den optionalen zweiten Parameter kann eine der folgenden Konstanten übergeben werden (jeweils mit Beispiel): DateFormat.GeneralDate DateFormat.LongDate DateFormat.ShortDate DateFormat.LongTime DateFormat.ShortTime
31.12.2002 16:55:37 Dienstag, 31. Dezember 2002 31.12.2002 16:55:37 16:55
Das folgende Beispiel demonstriert die Anwendung von FormatDateTime. s = FormatDateTime(Now, DateFormat.ShortTime)
's = "16:55"
VB-spezifische Codes für Format Grundsätzlich können bei Format dieselben Kürzel wie bei den .NET-Formatierungsmethoden verwendet werden. Diese Kürzel werden hier nicht nochmals aufgezählt – siehe Abschnitt 8.5.3! Das folgende Beispiel zeigt die Anwendung des Formatcodes d (kurzes Datumsformat). s = Format(Now, "d")
's = "31.12.2002"
Darüber hinaus kennt Format ein paar weitere Codes, deren Hauptvorteil darin besteht, dass ihre Bedeutung unmittelbar klar ist. (Dafür ist der Tippaufwand größer.) s = format(Now, "Medium Date")
's = "Dienstag, 31. Dezember 2002"
Die folgende Tabelle fasst die VB-spezifischen Codes zusammen und gibt jeweils ein Beispiel, das mit deutschen Landeseinstellungen ermittelt wurde. VB-spezifische Codes zur Formatierung von Zahlen (Methode Strings.Format) General Date
31.12.2002 16:55:37
Datum und Zeit
Long Date
Dienstag, 31. Dezember 2002
Datum ausführlich
Medium Date
Dienstag, 31. Dezember 2002
Datum mittel
Short Date
31.12.2002
Datum kompakt
Long Time
16:55:37
Zeit ausführlich
Medium Time
16:55:37
Zeit mittel
8.6 VB-Formatierungsmethoden
353
VB-spezifische Codes zur Formatierung von Zahlen (Methode Strings.Format) Short Time
16:55
Zeit kompakt
VB-Hilfsmethoden zur Formatierung von Daten und Zeiten Neben den Format-Methoden enthält die VB-Bibliothek auch einige Hilfsmethoden in der Klasse DateAndTime. Alle Methoden liefern Datum- und Zeitangaben gemäß der aktuellen Ländereinstellung. Methoden der Klasse Microsoft.VisualBasic.DateAndTime (siehe Abschnitt 8.3.1) DateString
liefert das aktuelle Datum (ohne Zeit) als Zeichenkette.
MonthName(n)
liefert den Monatsnamen (für n=1 bis 12).
MonthName(n, True)
liefert den Monatsnamen als Abkürzung (drei Buchstaben).
TimeString
liefert die aktuelle Uhrzeit als Zeichenkette.
WeekdayName(n)
liefert den Wochentag als Zeichenkette (n=1 für Sonntag).
WeekdayName(n, True)
liefert den Wochentag als Abkürzung (zwei Buchstaben)
9
Aufzählungen (Arrays, Collections)
Die .NET-Bibliothek enthält in den Namensräum System.Collections und System.Collections.Specialized eine große Anzahl von Klassen, die bei der Organisation von Aufzählungen helfen, beispielsweise ArrayList, SortedList, DictionaryBase etc. Damit können Sie assoziative Felder erzeugen, sortierte Listen effizient verwalten etc. Dieses Kapitel beschreibt, wie Sie von diesen Klassen abgeleiteten Objekten auswerten (z.B. wenn Sie derartige Aufzählungen als Ergebnis einer .NETMethode erhalten) und wie Sie die Klassen selbst zur Organisation Ihrer Daten einsetzen können.
VERWEIS
9.1 9.2 9.3 9.4
Einführung Klassen- und Schnittstellenüberblick Programmiertechniken Syntaxzusammenfassung
356 359 364 378
Der Umgang mit gewöhnlichen Feldern (die intern von System.Array abgeleitet sind), wird im Kapitel zum Thema Variablenverwaltung in Abschnitt 4.5 behandelt.
356
9.1
9 Aufzählungen (Arrays, Collections)
Einführung
Die einfachste Möglichkeit, mehrere Werte, Zeichenketten oder Objekte in einer Gruppe zu verwalten, bieten die in Abschnitt 4.5 bereits vorgestellten Felder. Felder haben aber den Nachteil, dass die Elementzahl bereits im Voraus bekannt sein muss und dass der Zugriff ausschließlich über einen Integer-Index erfolgen kann. (Wenn diese Einschränkungen für Sie kein Problem darstellen, sollten Sie wegen der großen Effizienz einfach bei Feldern bleiben.) Für viele Anwendungsfälle stellen Felder allerdings nicht die optimale Lösung dar. Deswegen gibt es im Namensraum System.Collections in mscorlib.dll sowie im Namensraum System.Collections.Specialized in System.dll eine ganze Reihe von so genannten Aufzählungsklassen, die die Verwaltung von Daten je nach Anwendung stark vereinfachen können. (Beachten Sie, dass System.Collections bei allen VB.NET-Projekttypen ein Defaultimport ist. Daher funktioniert Dim ht As Hashtable, obwohl Sie System.Collections.Hashtable meinen.) Damit Sie eine erste Vorstellung bekommen, wozu Aufzählungen eingesetzt werden können, beginnt dieses Kapitel mit einigen einfachen Beispielen.
ArrayList-Beispiel Zu den einfachsten Aufzählungen gehört ArrayList. Diese Klasse hat ähnliche Eigenschaften wie ein gewöhnliches Feld. Der Hauptunterschied besteht darin, dass Objekte nach Belieben eingefügt und wieder entfernt werden dürfen. ArrayList wird sehr oft dann eingesetzt, wenn eine (im Vorhinein noch unbekannte Anzahl) von Objekten oder Zeichenketten eingelesen und anschließend sortiert werden soll. In den folgenden Zeilen werden mit der Methode GetFiles alle Dateien im Verzeichnis c:\ ermittelt. (GetFiles liefert ein Feld von IO.FileInfo-Objekten – siehe Abschnitt 10.3.) Die Namen der Verzeichnisse werden mit Add in das ArrayList-Objekt eingefügt. Sort sortiert die Liste. Anschließend werden die Namen in einer For-Each-Schleife im Konsolenfenster ausgegeben. ' Beispiel aufzaehlungen\intro Sub arraylist_sample() Dim ar As New ArrayList() Dim fi As IO.FileInfo Dim o As Object ' alle Namen der Dateien in c:\ in die ArrayList eintragen For Each fi In New IO.DirectoryInfo("c:\").GetFiles() ar.Add(fi.Name) Next
9.1 Einführung
357
' ArrayList sortieren ar.Sort() ' ArrayList ausgeben For Each o In ar Console.WriteLine(o) Next End Sub
Die wenigen Zeilen demonstrieren bereits einige elementare Eigenschaften vieler CollectionKlassen: Vor der Verwendung der Klasse muss ein entsprechendes Objekt mit New erzeugt werden. (Bei ArrayList können Sie als optionalen Parameter die voraussichtliche Anzahl der Elemente angeben. Die tatsächliche Elementzahl darf sowohl größer als auch kleiner sein. Die Angabe einer sinnvollen Startgröße kann aber die interne Verwaltung ein bisschen effizienter machen, weil dann seltener eine automatische Anpassung an die erforderliche Elementzahl notwendig ist.) Elemente können mit Add eingefügt und bei Bedarf durch RemoveAt(n) oder Remove(obj) wieder entfernt werden. Die Elemente der Aufzählung können Sie mit Sort sortieren, mit Reverse auf den Kopf stellen etc. BinarySearch ermöglicht eine sehr effiziente Suche nach einzelnen Elementen. Der Zugriff auf die Aufzählungselemente erfolgt durch arraylistobj(n) oder mit For-EachSchleifen. Dabei müssen Sie beachten, dass Sie die Elemente immer mit dem Typ Object erhalten, unabhängig davon, welche Daten Sie tatsächlich gespeichert haben. Gegebenenfalls müssen Sie mit CType eine Umwandlung in den ursprünglichen Objekttyp vornehmen.
Hashtable-Beispiel Die Hashtable-Klasse bietet das, was in anderen Programmiersprachen oft als assoziatives Feld bezeichnet wird. Das bedeutet, dass Datenpaare verwaltet werden, die jeweils aus dem Schlüssel für den Datenzugriff und dem eigentlichen Datenobjekt bestehen. Das ermöglicht es wie im folgenden Beispiel, Zeichenketten für den Objektzugriff zu verwenden. Mit Add werden vier Objekte unterschiedlichen Typs in die Aufzählung eingefügt, wobei als Schlüssel die Zeichenketten "eins", "zwei" etc. verwendet werden. Der Zugriff auf einzelne Elemente kann nun in der Form ht(schlüsselobjekt) erfolgen. ht("vier") liefert daher ein Objekt des Typs System.Random. In der For-Each-Schleife werden alle Elemente der Aufzählung durchlaufen, wobei sowohl der Schlüssel als auch der Inhalt im Konsolenfenster ausgegeben werden. Beachten Sie, dass die Schleifenvariable ein Objekt des Typs DictionaryEntry sein muss. (Ein DictionaryEntry-Objekt liefert mit den Eigenschaften Key und Value die beiden Komponenten des Datenpaars.)
358
9 Aufzählungen (Arrays, Collections)
' Beispiel aufzaehlungen\intro Sub hashtable_sample() Dim ht As New Hashtable() Dim dictEntry As DictionaryEntry ht.Add("eins", ht.Add("zwei", ht.Add("drei", ht.Add("vier",
1) 23.5) "eine Zeichenkette") New Random())
Console.WriteLine("Element ht(""drei""): " + ht("drei").ToString) For Each dictEntry In ht Console.WriteLine("Key: " + dictEntry.Key.ToString + _ "Daten: " + dictEntry.Value.ToString) Next End Sub
.NET-Aufzählungen auswerten Zahlreiche .NET-Klassen verwenden intern ebenfalls die in diesem Kapitel beschriebenen Aufzählungsklassen. Daher überrascht es wenig, dass viele .NET-Methoden als Ergebnis auch derartige Aufzählungen zurückgeben. Eine Besonderheit besteht allerdings darin, dass solche Methoden oft nicht mit einem Klassentyp wie ArrayList oder Hashtable deklariert sind, sondern mit einer Schnittstelle wie IDictionary oder IList. Beispielsweise liefert die Methode Environment.GetEnvironmentVariables eine Liste aller Namen der Umgebungsvariablen des Betriebssystems samt deren Inhalt. Diese Liste wird als IDictionary-Objekt zur Verfügung gestellt. Was haben nun diese Schnittstellen mit den Aufzählungsklassen zu tun? Da die CollectionsKlassen zum Teil sehr ähnlich sind, haben die .NET-Entwickler versucht, den gemeinsamen Nenner dieser Klassen in Schnittstellen zu formulieren. Beispielsweise realisieren sowohl die ArrayList- als auch die Hashtable-Klasse die ICollection-Schnittstelle. Deswegen können Sie in beiden Fällen mit Count die Anzahl der Elemente feststellen oder mit CopyTo die Elemente in ein gewöhnliches Feld kopieren. Schnittstellen helfen also dabei, den Zugriff auf unterschiedliche Collections-Objekte möglichst einheitlich zu gestalten. Wenn Sie wie im konkreten Beispiel mit einer .NET-Methode konfrontiert sind, dessen Ergebnisobjekt mit einer Schnittstelle deklariert ist, dann können Sie zur Auswertung der Daten alle Methoden und Eigenschaften dieser Schnittstelle verwenden. (Für die wichtigsten Schnittstellen finden Sie eine Referenz dieser Schlüsselwörter in der Syntaxzusammenfassung am Ende dieses Kapitels.) In der Praxis sieht der resultierende Code meist genau gleich aus, als würde er ein gewöhnliches Collection-Objekt auswerten. Die unten abgedruckte For-Each-Schleife entspricht in ihrer Struktur exakt dem vorigen Hashtable-Beispiel.
9.2 Klassen- und Schnittstellenüberblick
359
' Beispiel aufzaehlungen\intro Sub read_idictionary() Dim entry As DictionaryEntry For Each entry In Environment.GetEnvironmentVariables() Console.WriteLine("{0} = {1}", _ entry.Key.ToString, entry.Value.ToString) Next End Sub
HINWEIS
Es ist unmöglich, ein Objekt direkt von einer Schnittstelle zu erzeugen. Daher kann GetEnvironmentVariables genau genommen auch kein IDictionary-Objekt liefern. Aber indem der Typ der Rückgabedaten von GetEnvironmentVariable mit IDictionary deklariert ist, weiß der Compiler und wissen Sie, dass zumindest alle Eigenschaften und Methoden der IDictionary-Schnittstelle zur Verfügung stehen. Die Bezeichnung IDictionary-Objekt ist insofern also nicht ganz korrekt, aber eben auch nicht ganz falsch (und sicherlich einfacher zu verstehen, als wenn ich jedes Mal schreibe: ein Objekt, dessen Klasse die IDictionary-Schnittstelle realisiert). Falls es Sie interessiert, von welcher Klasse dieses Objekt intern nun tatsächlich abgeleitet ist, können Sie das mit TypeName(Environment.GetEnvironmentVariables) feststellen. Bei GetEnvironment handelt es sich um ein Objekt der Klasse Hashtable.
9.2
Klassen- und Schnittstellenüberblick
Wie in der Einführung bereits erwähnt wurde, spielen Schnittstellen eine wichtige Rolle bei der Definition der Merkmale, die eine bestimmte Klasse unterstützen. Bevor dieser Abschnitt daher einen Überblick über die Merkmale der wichtigsten Collections-Klassen gibt, müssen Sie vorher zumindest die wichtigsten Schnittstellen kennen lernen. (Soweit nicht anders angegeben, sind alle in diesem Abschnitt genannten Schnittstellen und Klassen im Namensraum System.Collections der mscorlib.dll definiert.)
Schnittstellenüberblick •
IEnumerable: Diese Schnittstelle ermöglicht es, dass alle Elemente einer Aufzählung komfortabel in einer For-Each-Schleife angesprochen werden können. (Intern liefert dazu die Methode GetEnumerator ein IEnumerator-Objekt, das bei der Ausführung der For-Each-Schleife ausgewertet wird.)
Beachten Sie, dass der Datentyp der Elemente der For-Each-Schleife von der konkreten Realisierung von GetEnumerator abhängt! In vielen Fällen liefert die For-Each-Schleife einfach Object-Daten; bei allen Klassen, die die IDictionary-Schnittstelle realisieren, sollte die Schleifenvariable aber mit dem Typ DictionaryEntry deklariert werden; bei Aufzählungen auf Basis der StringCollection-Klasse bekommen Sie dagegen String-Daten etc.
360
•
9 Aufzählungen (Arrays, Collections)
ICollection: Diese Schnittstelle ermöglicht die Ermittlung der Elementzahl (Count) und
das Kopieren der Elemente in ein einfaches Feld. •
IList: Diese Schnittstelle stellt Methoden zur Verfügung, die den Zugriff und die Verwaltung einfacher Listen ermöglichen (Add, Remove, Contains etc.) Besonders wichtig ist die Eigenschaft Item(n) zum Zugriff auf ein Element der Aufzählung. Item(n) wird selten
explizit verwendet, weil VB.NET diese Eigenschaft intern verwendet, wenn Sie auf Aufzählungselemente in der Form aufzählungsobjekt(n) zugreifen. •
IDictionary: Diese Schnittstelle ist eine Alternative zu IList und hilft bei der Verwaltung
von Datenpaaren. Der entscheidende Unterschied besteht darin, dass der Elementzugriff nun über beliebige Schlüsselobjekte erfolgt (wobei intern ein Hash-Code zu Hilfe genommen wird). Die Schnittstellen stehen in einem hierarchischen Zusammenhang, der aus der folgenden Hierarchiebox hervorgeht. Beispielsweise realisiert jede IDictionary-Schnittstelle automatisch auch die Schnittstellen ICollection und IEnumerable. Hierarchie der System.Collections-Schnittstellen IEnumerable └─ ICollection ├─ IDictionary └─ IList
For-Each-Unterstützung Count, CopyTo assoziativer Zugriff auf Elemente, Add und Remove Indexzugriff (Integer) auf Elemente, Add und Remove
Die folgende Tabelle gibt für die wichtigsten Aufzählungsklassen an, welche der vier obigen Schnittstellen implementiert werden. IEnumerable
ICollection
IList
+ +
+ +
+ +
+ +
+ +
+
HybridDictionary
+ +
+ +
+ +
StringDictionary
+
NameValueCollection
+ +
+ +
+
Array ArrayList StringCollection Hashtable ListDictionary
SortedList
IDictionary
+
Klassenüberblick Wenn Sie Objekte in einer Aufzählung verwalten möchten, können Sie dazu nicht direkt eine Schnittstelle verwenden. Vielmehr müssen Sie auf Klassen zurückgreifen, die diese Schnittstellen realisieren. Dieser Abschnitt stellt die wichtigsten derartigen Klassen vor.
9.2 Klassen- und Schnittstellenüberblick
361
Um das Bild abzurunden, werden in diesem Abschnitt auch drei Möglichkeiten außerhalb der Collections-Klassen genannt: gewöhnliche Felder, Datenverwaltung mit ADO.NET sowie XML-Dokumente. •
Felder: Die in Abschnitt 4.5 bereits beschriebenen Felder sind intern von der Klasse System.Array abgeleitet. Sie bieten die effizienteste Möglichkeit, große Datenmengen von Werttypen (ValueType-Klassen) zu speichern – also z.B. eine Million Integerzahlen. Der Zugriff auf die Elemente erfolgt über einen numerischen Index. Felder werden im angegebenen Datentyp deklariert (z.B. String). Es besteht die Möglichkeit, mehrdimensionale Felder zu verwalten. Nachteile: Felder müssen im Voraus in der richtigen Größe dimensioniert werden. Eine nachträgliche Änderung ist zwar möglich, aber relativ langsam. Es ist nicht möglich, Elemente einzufügen oder zu entfernen.
•
ArrayList: Diese Klasse weist ähnliche Eigenschaften wie ein normales Feld auf. Der
wesentliche Vorteil besteht darin, dass die Aufzählung automatisch wächst oder schrumpft, wenn Elemente mit Add oder Insert eingefügt bzw. mit Remove[At] entfernt werden. Der Zugriff erfolgt über einen Index, also aufzählung(n). Die Aufzählung kann sortiert werden, sortierte Aufzählungen können anschließend sehr effizient durchsucht werden. Im Vergleich zu Feldern bietet die Klasse Methoden wie InsertRange, RemoveRange oder GetRange, um mehrere Elemente gleichzeitig einzufügen, zu löschen oder in ein neues ArrayList-Objekt zu kopieren. (Der Vorteil dieser Methoden liegt vor allem in der höheren Geschwindigkeit.) Nachteile: Wie fast alle Collection-Klassen sind die Elemente der Aufzählung als Object deklariert. Das ist ideal zur Speicherung von Referenztyp-Objekten. Allerdings müssen elementare Datentypen wie Integer oder Double (und generell alle ValueType-Daten) durch ein so genanntes boxing in richtige Objekte umgewandelt werden. Alle CollectionKlassen sind daher im Vergleich zu Feldern ineffizient, wenn große ValueType-Datenmengen verwaltet werden müssen. •
StringCollection: Wenn Sie nicht allgemeine Objekte, sondern nur Zeichenketten verwalten möchten, können Sie statt ArrayList ein Objekt der Klasse Collections.Specialized.StringCollection verwenden. Diese Klasse ist speziell für Zeichenketten optimiert und
daher ein wenig effizienter. Nachteile: Im Gegensatz zu ArrayList bietet StringCollection keine Sortier- und Suchmethoden. •
Hashtable: Diese Klasse ermöglicht es, so genannte assoziative Felder zu erzeugen. Das sind Felder, bei denen als Schlüssel für den Elementzugriff nicht ein Integer-Index,
sondern ein beliebiges Objekt (z.B. eine Zeichenkette) verwendet werden kann. Zur Verwaltung der Elemente wird der so genannte hash-Wert der Schlüsselobjekte verwendet. Intern wird dazu die Methode GetHashCode ausgeführt, die automatisch für jede Klasse zur Verfügung steht. GetHashCode liefert bei den elementaren Datentypen (Zahlen, Zeichenketten etc.) gut gleichverteilte Werte.
362
9 Aufzählungen (Arrays, Collections)
Bei selbst erstellten Klassen liefert die Methode allerdings lediglich eine durchlaufende Nummer. Damit ist zwar sichergestellt, dass jedes Objekt einer Klasse einen eindeutigen hash-Wert hat, aber von Gleichverteilung ist jetzt keine Rede mehr. Außerdem liefern zwei Objekte mit exakt denselben Daten unterschiedliche hash-Werte. (Im Regelfall wäre es sinnvoller, wenn Objekte mit denselben Daten auch denselben hash-Wert hätten.) Wenn Sie also Objekte eigener Klassen als Schlüssel in einer Hashtable verwenden, sollte die Klasse eine eigene GetHashCode-Methode zur Verfügung stellen, um eine effiziente Datenverarbeitung sicherzustellen. Wie bei ArrayList können Elemente mit Add eingefügt und mit Remove entfernt werden. Beim Einfügen von Elementen in eine Hashtable muss darauf geachtet werden, dass derselbe Schlüssel nicht zweimal verwendet wird. Mit der Methode ContainsKeys kann getestet werden, ob ein bestimmter Schlüssel bereits im Einsatz ist. Nachteile: Im Vergleich zu ArrayList besteht keine Möglichkeit, die Elemente der Aufzählung zu sortieren oder nach Elementen zu suchen. Auch die effizienten XxxRangeKommandos fehlen. Es gibt keine Möglichkeit, über einen numerischen Index auf die Elemente zuzugreifen. •
ListDictionary: Wenn Sie überwiegend sehr kleine Aufzählungen verwalten (bis zu zehn Elementen) und dabei Wert auf maximale Effizienz legen, bietet die Klasse Collections.Specialized.ListDictionary beinahe dieselben Eigenschaften wie Hashtable. Der Vorteil
besteht darin, dass der Verwaltungsaufwand geringer ist. •
HybridDictionary: Wenn Sie sich nicht zwischen Hashtable und ListDictionary entscheiden können, ist Collections.Specialized.HybridDictionary die richtige Wahl. Diese Klasse beginnt als ListDictionary, wechselt intern aber zu einer Hashtable, wenn die Elementzahl stark steigt.
•
StringDictionary: Die Klasse Collections.Specialized.StringDictionary ist eine weitere Variante zu Hashtable. Diesmal besteht die Besonderheit darin, dass sowohl der Schlüssel als
auch die Daten ausschließlich Zeichenketten sein können. Der Vorteil besteht abermals in der etwas höheren Effizienz. •
NameValueCollection: Diese Klasse ist eine Sonderform von StringDictionary. Abermals müssen Schlüssel und Daten jeweils Zeichenketten sein. Die Besonderheit besteht darin, dass der Schlüssel nicht eindeutig sein muss, d.h., zu einer Schlüsselzeichenkette können mehrere Datenzeichenketten gespeichert werden.
•
SortedList: Diese Klasse vereint die Merkmale von gewöhnlichen Feldern und der Hashtable-Klasse. Wie bei Hashtable gibt es also einen assoziativen Zugriff auf die gespeicher-
ten Objekte. Darüber hinaus besteht aber die Möglichkeit, auf die einzelnen Elemente und Schlüssel über eine Indexnummer zuzugreifen. Dazu werden alle Elemente automatisch bei jedem Einfüge- bzw. Löschvorgang nach ihren Schlüsseln sortiert. (Bei der New-Methode kann ein IComparer-Objekt angegeben werden, dessen Compare-Methode zum Sortieren verwendet wird.) Nachteil: Intern werden dazu zwei Felder für die Elemente und die Schlüsseln verwaltet, d.h., der Verwaltungsaufwand (vor allem beim Einfügen und Entfernen von Ele-
9.2 Klassen- und Schnittstellenüberblick
363
menten) ist deutlich größer als bei der Hashtable-Klasse. Dies gilt insbesondere für Aufzählungen mit sehr vielen Elementen. •
DataSet (ADO.NET): Auch die Datenbankbibliothek ADO.NET und insbesondere die Klasse DataSet bieten viele Möglichkeiten zur Verwaltung von Daten. ADO.NET ist dann optimal, wenn Sie sehr große Datenmengen in einer externen Datenbank speichern möchten und bei Zugriffen nicht immer die gesamte Datenmenge, sondern nur einzelne Teile lesen möchten.
Nachteile: ADO.NET ist erheblich komplizierter anzuwenden als die in diesem Kapitel beschriebenen Collection-Klassen. Wirklich optimal ist die Anwengung nur in Kombination mit einem externen Datenbanksystem (z.B. SQL-Server oder die Jet-Engine). •
XmlDocument: Diese Klasse aus dem Namensraum System.XML ermöglicht es, ein XML-
Dokument im Speicher abzubilden. Sie können diese Klasse natürlich zum Lesen und Schreiben von XML-Dokumenten verwenden – Sie können die in XML vorgesehenen Mechanismen aber auch zur Verwaltung von Daten einsetzen. Der entscheidende Vorteil gegenüber den Collections-Klassen besteht darin, dass sich XML ausgezeichnet zur Darstellung hierarchischer Zusammenhänge eignet.
HINWEIS
Nachteile: XML bringt eine Menge Overhead mit sich, der sich sowohl im vergleichsweise hohen Speicherbedarf als auch in der relativ geringen Verarbeitungsgeschwindigkeit negativ bemerkbar macht. Die Anwendung der XmlDocument-Klasse ist daher nur sinnvoll, wenn Sie die spezifischen XML-Merkmale wirklich benötigen (oder wenn Sie ohnedies vorhaben, die Daten in einer XML-Datei zu speichern). Neben den aufgezählten Collections-Klassen gibt es noch ein paar Sonderformen, auf deren genauere Beschreibung hier verzichtet wird: Queue ermöglicht die Realisierung einfacher Warteschlangen. Stack realisiert einen first-in-first-out-Puffer (FIFO). BitArray hilft dabei, ein effizientes (Platz sparendes) Feld für Bitwerte zu bilden. BitVector32 bietet ähnliche Funktionen wie BitArray, ist aber auf 32 Bit beschränkt.
VERWEIS
Wenn es Ihnen anhand dieser Beschreibung und den folgenden Beispielen schwer fällt, sich für die richtige Collection-Klasse zu entscheiden, sollten Sie sich als Erstes mit den beiden Klassen ArrayList und Hashtable anfreunden. Für mindestens 90 Prozent aller Anwendungen reichen diese beiden Klassen aus. Einen guten Überblick über die Merkmale der verschiedenen Klassen gibt die Übersichtstabelle in Abschnitt 9.4.2. Hilfreich ist auch die Seite Auswählen einer Auflistungsklasse in der Online-Hilfe: ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpconselectingcollectionclass.htm
Dort finden Sie eine systematische Anleitung zur Auswahl der richtigen Klasse anhand mehrerer Entscheidungsfragen.
364
9 Aufzählungen (Arrays, Collections)
Aufzählungen selbst programmieren Die oben aufgezählten Collection-Klassen können unmittelbar eingesetzt werden. Es kann aber sein, dass deren Eigenschaften Ihrer Anwendung nicht optimal entsprechen – dann müssen Sie selbst eine Aufzählungsklasse programmieren. Damit Sie auch in diesem Fall das Rad nicht neu erfinden müssen, stehen Ihnen einige XxxBase-Klassen zur Auswahl, auf die Sie Ihren Code aufbauen können. •
CollectionBase: Die Klasse hilft bei der Programmierung von Aufzählungen, die die IList-Schnittstelle realisieren (wie ArrayList).
•
ReadOnlyCollectionBase: Die Klasse erleichtert die Programmierung von Read-OnlyAufzählungen auf das Basis von IList. Im Vergleich zu CollectionBase müssen viel weni-
ger Methoden selbst programmiert werden. •
DictionaryBase: Die Klasse hilft bei der Verwaltung assoziativer Felder auf der Basis der IDictionary-Schnittstelle (wie Hashtable).
•
NameObjectCollectionBase: Damit können Sie eine Klasse bilden, bei der der Elementzugriff wahlweise über einen Index oder über einen String-Schlüssel erfolgt. Intern werden die Schlüsseleinträge sortiert (wie SortedList).
TIPP
VERWEIS
Trotz dieser Basisklassen ist die Programmierung eigener Aufzählungen ziemlich aufwendig. Insbesondere müssen alle Methoden für den Elementzugriff neu implementiert werden, um die Verwendung der korrekten Datentypen sicherzustellen. Um einen komfortablen For-Each-Zugriff auf die Elemente zu ermöglichen, muss des Weiteren eine eigene Klasse programmiert werden, die die IEnumerator-Schnittstelle realisiert.
9.3
Beispiele für die Programmierung eigener Aufzählungsklassen finden Sie unter der folgenden Adresse: http://www.wintellect.com/resources/newsletters/feb2002.asp
Falls Sie SharpDevelop installiert haben (siehe Abschnitt 2.7.2), können Sie dessen Assistenten dazu verwenden, um den Code einer eigenen Collection-Klasse für einen bestimmten Datentyp bequem und rasch zu erzeugen (DATEI|NEU, Kategorie VB, Schablone GETYPTE VB COLLECTION). Den resultierenden Code können Sie anschließend in Ihr VB.NET-Projekt kopieren.
Programmiertechniken
Grundsätzlich hätte ich diesen Abschnitt damit beginnen können, die wichtigsten Schlüsselwörter aller Collections-Schnittstellen und -Klassen im Detail zu beschreiben. Es erschien mir aber sinnvoller, den Platz stattdessen zu nutzen, um einige konkrete Programmiertechniken vorzustellen – z.B. welche Möglichkeiten es gibt, Elemente einer Aufzählung zu
9.3 Programmiertechniken
365
VERWEIS
sortieren. Eine Referenz der wichtigsten Schlüsselwörter samt einer knappen Beschreibung finden Sie in der Syntaxzusammenfassung im nächsten Abschnitt. Bei den meisten Methoden (soweit sie nicht ohnedies im Rahmen der folgenden Beispiele vorgestellt wurden) wird die Anwendung nicht schwer fallen. Wie der Inhalt eines gesamten Felds oder eines Collection-Objekts dank Serialisierung effizient in einer Datei gespeichert bzw. von dort wieder geladen werden kann, erklärt Abschnitt 10.9.
9.3.1
Elemente einer Aufzählung einzeln löschen
Um alle Elemente einer Aufzählung zu löschen, steht bei den meisten Aufzählungsklassen die Methode Clear zur Verfügung. Falls Sie diese Methode nicht anwenden möchten (etwa weil Sie vor dem Löschen jedes Element noch bearbeiten möchten), müssen Sie die Aufzählung mit einer Schleife durchlaufen. Dabei ist aber Vorsicht geboten! Der naheliegende Ansatz, einfach alle Elemente in einer For-Each-Schleife zu löschen, führt zu einem Fehler. Der Grund: Die IEnumerator-Schnittstelle funktioniert nur, solange die Anzahl der Elemente nicht verändert wird. (Das gilt nicht nur für Änderungen innerhalb der For-Each-Schleife, sondern auch für mögliche simultane Änderungen durch andere Threads!) Bei Aufzählungen, deren Elemente über einen Integer-Index angesprochen werden können (z.B. ArrayList-Objekte auf der Basis der IList-Schnittstelle), bieten sich folgende Möglichkeit an. (Statt list.RemoveAt(list.Count-1) funktioniert auch list.RemoveAt(0). Die hier vorgestellte Variante kommt der internen Organisation der Daten aber eher entgegen.) ' Beispiel aufzaehlungen\intro Sub delete_items_list(ByVal list As IList) While list.Count > 0 list.RemoveAt(list.Count-1) End While End Sub
Bei Aufzählungen auf der Basis von IDictionary-Schnittstelle können Sie wie in den folgenden Zeilen zuerst alle Schlüsselobjekte in ein Feld kopieren und dann alle Elemente dieses Felds durchlaufen. Allerdings muss sichergestellt sein, dass sich die Aufzählung während der Ausführung der Schleife nicht ändert. Sub delete_items_dict(ByVal dict As IDictionary) Dim i As Integer Dim k(dict.Count - 1) As Object ' kopiert alle Schlüssel in das Feld k dict.Keys.CopyTo(k, 0) For i = dict.Count - 1 To 0 Step -1 dict.Remove(k(i)) Next End Sub
366
9 Aufzählungen (Arrays, Collections)
9.3.2
Elemente einer Aufzählungen sortieren
Wenn Sie die Elemente einer Aufzählung sortieren möchten, kommt als einzige CollectionsKlasse ArrayList in Frage. Eine Alternative zu ArrayList bilden gewöhnliche Felder, deren Klasse Array ebenfalls eine Sort-Methode kennt. (Bei der Klasse SortedList werden dagegen nicht die eigentlichen Daten, sondern die Schlüssel sortiert – siehe den nächsten Abschnitt!) Bei den folgenden Zeilen wird zur Initialisierung der ArrayList an den New-Konstruktor ein String-Feld übergeben. Zum Sortieren wird einfach die Methode Sort angegeben. ' Beispiel aufzaehlung\sort_data Dim al As New ArrayList( _ New String() {"abc", "ABC", "Abcd", "bar", _ "Bär", "Bären", "Barenboim", _ "bärtig", "27", "100", "Ärger"}) al.Sort()
Wenn Sie sich die Elemente der sortierten ArrayList ansehen, ergibt sich folgende Reihenfolge: 100
27
abc
ABC
Abcd
Ärger
bar
Bär
Bären
Barenboim
bärtig
Sort sortiert per Default im deutschen Sprachraum so, dass es Groß- und Kleinschreibung als fast gleichwertig betrachtet: Ein kleines a wird vor einem großen A sortiert, a und A aber beide vor b etc. Auch deutsche Sonderzeichen werden korrekt berücksichtigt. (Hinter-
gründe über den Vergleich von Zeichenketten finden Sie in Abschnitt 8.2.3.)
IComparable-Schnittstelle Das Sortieren durch Sort funktioniert nur dann, wenn die in der Aufzählung enthaltenen Objekte die IComparable-Schnittstelle unterstützen. Für die Basisdatentypen wie Integer, String oder Date ist das automatisch der Fall. Wenn Sie in einer Aufzählung aber Objekte einer selbst definierten Klasse sortieren möchten, müssen Sie diese Klasse entweder selbst mit dieser Schnittstelle ausstatten, oder Sie müssen an Sort ein Objekt übergeben, das eine IComparer-Schnittstelle realisiert (siehe die nächste Überschrift). Die folgenden Zeilen definieren das Objekt vector2d, das zur Speicherung von zweidimensionalen Vektoren dient. Der Großteil des Codes sollte ohne weitere Erklärung verständlich sein: New ermöglicht eine bequeme Initialisierung des Objekts; Len berechnet die Länge des Vektors; ToString gibt das Objekt in Textform aus. Besonders von Interesse ist hier die Implementierung der IComparable-Schnittstelle: Dazu muss die Klasse die Methode CompareTo zur Verfügung stellen. Die Methode vergleicht das Objekt mit einem zweiten Objekt derselben Klasse. Im Beispiel erfolgt der Vergleich entsprechend der Länge der beiden Vektoren, wobei auf die CompareTo-Methode der Double-Klasse zurückgegriffen wird. Falls das Vergleichsobjekt von einer anderen Klasse abstammt, wird eine ArgumentException ausgelöst.
9.3 Programmiertechniken
367
' Beispiel aufzaehlung\sort_data Class vector2d Implements IComparable Dim x As Double Dim y As Double ' New-Konstruktor Public Sub New() Me.New(0, 0) End Sub Public Sub New(ByVal x As Double, ByVal y As Double) Me.x = x Me.y = y End Sub ' berechnet die Länge des Vektors Public Function Len() As Double Return Math.Sqrt(x ^ 2 + y ^ 2) End Function ' ToString-Methode Public Overrides Function ToString() As String Return "(" + Me.x.ToString + "; " + Me.y.ToString + ")" End Function ' CompareTo-Methode der IComparable-Schnittstelle Overridable Function CompareTo( _ ByVal obj As Object) _ As Integer Implements IComparable.CompareTo If TypeOf obj Is vector2d Then Return Me.Len().CompareTo(CType(obj, vector2d).Len()) Else Throw New ArgumentException() End If End Function End Class
Am schwierigsten zu verstehen ist wahrscheinlich die folgende Zeile des Programmcodes: Return Me.Len().CompareTo(CType(obj, vector2d).Len()) Me.Len() liefert einen Double-Wert, der die Länge der aktuellen Objektinstanz liefert. Dieser Wert soll mit der Länge von obj verglichen werden. Obwohl bereits überprüft wurde, dass obj ein Objekt der Klasse vector2d ist, betrachtet der Compiler obj als Object (weil der Parameter ja so deklariert ist). obj.Len() ist deswegen unzulässig, weil die Klasse Object keine Methode Len kennt. obj muss also mit CType(obj, vector2d) in ein vector2d-Objekt umgewandelt werden. (Diese Typenkonvertierung ist nur für den Compiler wichtig. Tatsächlich enthält obj ja bereits Daten im richtigen Typ. CType(obj, vector2d) verursacht daher keinen Rechenaufwand.) Auf
368
9 Aufzählungen (Arrays, Collections)
das Ergebnis von CType kann nun die Len-Methode angewendet werden, die einen DoubleWert liefert. Me.Len().CompareTo vergleicht somit zwei Double-Werte. Auf der Basis dieser Klasse stellt es nun kein Problem dar, mehrere vector2d-Objekte zu erzeugen, zu sortieren und mit der (nicht abgedruckten) Prozedur print_ilist in einem Konsolenfenster auszugeben. Sub sort_icomparable() Dim ar As New ArrayList() ar.Add(New vector2d(3, 4)) ar.Add(New vector2d(2.9, 4.1)) ar.Add(New vector2d(3.1, 3.9)) ar.Add(New vector2d()) ar.Sort() print_ilist(ar) End Sub
Das Beispielprogramm liefert das folgende Ergebnis: (0; 0)
(3,1; 3,9)
(3; 4)
(2,9; 4,1)
IComparer-Schnittstelle
HINWEIS
Wenn die durch CompareTo vorgegebene Defaultordnung beim Sortieren nicht Ihren Wünschen entspricht oder wenn Sie Objekte von Klassen sortieren möchten, die die IComparableSchnittstelle nicht unterstützen, dann können Sie an Sort ein Objekt übergeben, dessen Klasse die Schnittstelle Collections.IComparer realisiert. Verwechseln Sie IComparer und IComparable nicht! IComparer ist eine externe Schnittstelle, deren Objekt an Sort übergeben wird. Der Vergleich erfolgt durch die Methode Compare, an die zwei Objekte übergeben werden. Im Gegensatz dazu ist IComparable eine Schnittstelle, die innerhalb der Klasse definiert wird. Die Methode CompareTo vergleicht die konkrete Objektinstanz (also Me) mit einem externen Objekt der gleichen Klasse.
Das Ziel des folgenden Beispiels besteht darin, mehrere Zeichenketten mit Namen in der Form "Stephen King" entsprechend der Familiennamen zu sortieren. Um das zu ermöglichen, wurde die Klasse CompareByLastName definiert. Dazu muss die Klasse die Methode Compare anbieten, an die zwei Object-Parameter übergeben werden. Da in diesem Beispiel nur Zeichenketten sortiert werden, können die Objekte mit CType in Zeichenketten umgewandelt werden. Die Funktion last_name_first überprüft nun, ob der Name ein Leerzeichen enthält. Wenn das der Fall ist, wird der letzte Teil des Namens an den Anfang gestellt. Die Funktion macht also aus "John F. Kennedy" die Zeichenkette "Kennedy John F.". Damit enthalten s1 und s2 zwei Namen, bei denen der Familienname am Anfang steht.
9.3 Programmiertechniken
369
Die resultierenden Zeichenketten können nun einfach mit String.Compare verglichen werden. (Diese Methode liefert -1, 0 oder 1, je nachdem, wie die alphabetische Reihenfolge der Zeichenketten ist. An String.Compare kann mit optionalen Parametern angegeben werden, welche Sortierordnung beim Vergleich berücksichtigt werden soll – siehe Abschnitt 8.2.3.) Class CompareByLastName Implements Collections.IComparer Overridable Function Compare( _ ByVal obj1 As Object, ByVal obj2 As Object) As Integer _ Implements IComparer.Compare Dim s1, s2 As String s1 = last_name_first(CType(obj1, String)) s2 = last_name_first(CType(obj2, String)) Return String.Compare(s1, s2) End Function Private Function last_name_first(ByVal s As String) As String Dim pos As Integer Dim lastname, firstname As String s = Trim(s) pos = InStrRev(s, " ") If pos > 0 Then lastname = Trim(Mid(s, pos + 1)) firstname = Trim(Left(s, pos - 1)) If lastname "" Then Return lastname + " " + firstname Else Return s End If Else Return s End If End Function End Class
Zum Test dieser Klasse werden einige Zeichenketten mit den Autorennamen verschiedener VB.NET-Bücher initialisiert. Um diese Namen nach den Vornamen zu sortieren, reicht ein einfacher Aufruf der Sort-Methode. Zum Sortieren nach Familiennamen muss beim Aufruf von Sort ein Objekt der Klasse CompareByLastName übergeben werden. Für die Ausgabe der Aufzählungselemente im Konsolenfenster wird die nicht abgedruckte Prozedur print_ilist verwendet.
370
9 Aufzählungen (Arrays, Collections)
Sub sort_icomparer() ' mehrere Zeichenketten in ein String-Feld und in eine ' ArrayList einfügen Dim s() As String = _ {"Holger Schwichtenberg", "Frank Eller", "Dan Appleman", _ "Brian Bischof", "Gary Cornell", "Jonathan Morrison", _ "Andrew Troelsen"} Dim al As New ArrayList(s) Console.WriteLine("unsortiert:") print_ilist(al) al.Sort() Console.WriteLine("normal sortiert:") print_ilist(al) Console.WriteLine("nach Familiennamen sortiert:") al.Sort(New CompareByLastName()) print_ilist(al) End Sub
Das Ergebnis des Programms sieht folgendermaßen aus: unsortiert: Holger Schwichtenberg Frank Eller Dan Appleman Gary Cornell Jonathan Morrison Andrew Troelsen
Brian Bischof
normal sortiert: Andrew Troelsen Brian Bischof Dan Appleman Frank Eller Gary Cornell Holger Schwichtenberg Jonathan Morrison
VERWEIS
nach Familiennamen sortiert: Dan Appleman Brian Bischof Gary Cornell Frank Eller Jonathan Morrison Holger Schwichtenberg Andrew Troelsen
Sie finden in diesem Buch noch eine ganze Reihe weiterer Beispiele für das Sortieren unterschiedlicher Daten: Werfen Sie einen Blick in das Stichwortverzeichnis bei den Einträgen sortieren und ICompare-Schnitstelle.
Nach Elementen suchen Wenn Sie ein bestimmtes Element in einer Aufzählung suchen, müssen Sie in der Regel alle Elemente durchlaufen. Wenn die Aufzählung aber bereits sortiert ist, können Sie die Suche ganz wesentlich beschleunigen, indem Sie die Methode BinarySearch zu Hilfe nehmen. Diese Methode benötigt beispielsweise zur Suche in einem Feld mit 1000 Einträgen nur maximal zehn Vergleichvorgänge.
9.3 Programmiertechniken
371
Im einfachsten Fall wird an die Methode ein eindimensionales, durch Sort sortiertes Feld bzw. eine Aufzählung sowie der Suchbegriff übergeben. BinarySearch liefert dann entweder die positive Indexnummer des gefunden Elements oder eine negative Nummer, wenn das Element nicht gefunden wurde; in diesem Fall können Sie das Vorzeichen umdrehen und gelangen dann zum nächstliegenden größeren oder kleineren Eintrag im Feld. Dim i As Integer i = al.BinarySearch("Dan Appleman") Console.WriteLine("Suche nach Dan Appleman: Index = " + _ i.ToString + " Element = " + al(i).ToString)
Wenn Sie das Feld mit einer eigenen Vergleichsmethode sortiert haben, müssen Sie auch an BinarySearch ein IComparer-Objekt übergeben: i = al.BinarySearch("Dan Appleman", New CompareByLastName())
Sortieren und Suchen bei Feldern Grundsätzlich stehen die Methoden Sort und BinarySearch auch für Felder zur Verfügung. Unverständlicherweise sieht aber die Syntax anders aus: Während bei ArrayList die Methode einfach nach dem Objektnamen angegeben wird, müssen hier Felder als Parameter der Methode übergeben werden: Dim i As Integer Dim s() As String = {...} Dim ar As New ArrayList(s) Array.Sort(s) Array.Sort(s, icompobj) i = Array.BinarySearch(s, "xy") i = Array.BinarySearch(s, "xy", icobj)
9.3.3
'entspricht ar.Sort() ' ar.Sort(icompobj) ' ar.BinarySearch("xy") ' ar.BinarySearch("xy", icobj)
Schlüssel einer Aufzählung sortieren
Im vorigen Abschnitt wurde gezeigt, wie die Elemente eines Felds oder einer ArrayList sortiert werden können. Bei Aufzählungsklassen, die direkt auf der IDirectory-Schnittstelle basieren, ist ein Sortieren grundsätzlich unmöglich, weil es keinen durchlaufenden Index zum Objektzugriff gibt. Eine Sortiermöglichkeit bietet nur die Spezialklasse SortedList. Jedes Mal, wenn Sie in ein Objekt dieser Klasse ein Element einfügen oder eines löschen, wird die Reihenfolge der Elemente reorganisiert. Sortiert werden dabei nicht die eigentlichen Daten, sondern die Schlüssel der Datenpaare. Dabei wird per Default die CompareTo-Methode der Objekte verwendet. Alternativ können Sie beim Erzeugen des SortedList-Objekts auch ein IComparerObjekt übergeben, dessen Compare-Methode dann ein individuelles Sortieren ermöglicht (siehe auch den vorigen Abschnitt).
372
9 Aufzählungen (Arrays, Collections)
Beispiel Das folgende Beispiel ermittelt unter Zuhilfenahme der System.Drawing.KnownColor-Aufzählung alle im Grafiksystem GDI bekannten Farben. Dazu ganz kurz einige Hintergrundinformationen: KnowColor enthält für mehr als 150 vordefinierte Farbcodes (einfache Integer-Zahlen). Aus diesen Konstanten können mit der Methode Drawing.Color.FromKnownColor Objekte des Typs Color ermittelt werden. (Mehr Informationen zur Verwendung von Farben finden Sie in Kapitel 16.) Die erste For-Each-Schleife hat also den Zweck, für alle bekannten Farben deren Namen (ein String-Objekt) und die Farbe an sich (ein Color-Objekt) zu ermitteln. Diese beiden Informationen werden mit Add in das SortedList-Objekt eingefügt, wobei der Farbname mit LCase in Kleinbuchstaben umgewandelt wird. Die folgenden Zeilen zeigen einige Möglichkeiten zur Auswertung. Mit sl("farbname") erhalten Sie ein Color-Objekt. Da SortedList aber allgemeingültig für den Datentyp Object deklariert ist, muss mit CType explizit eine Typumwandung durchgeführt werden, damit auch der Compiler erkennt, dass das Objekt die Klasse Color und nicht Object hat. Wenn Sie auf die Elemente in sl mit GetByIndex bzw. GetKey über einen Index zugreifen, sind die Elemente automatisch nach ihrem Namen geordnet. ' Beispiel aufzaehlungen\sort_key ' das Beispiel setzt eine Referenz auf die Bibliothek ' System.Drawing voraus Sub sortedlist_sample() Dim i As Integer Dim colorName As String Dim kc As Drawing.KnownColor Dim col As Drawing.Color Dim sl As New SortedList() ' bildet eine SortedList aller Farbnamen mit den ' dazugehörenden Color-Objekten For Each colorName In System.Enum.GetNames(kc.GetType()) kc = CType(System.Enum.Parse(kc.GetType(), colorName), _ Drawing.KnownColor) col = Drawing.Color.FromKnownColor(kc) sl.Add(LCase(colorName), col) Next ' Auswertung der SortedList Console.WriteLine("sl enthält {0} Farben", sl.Count) col = CType(sl("red"), Drawing.Color) Console.WriteLine("Farbe red: R/G/B = {0}/{1}/{2}", _ col.R, col.G, col.B)
9.3 Programmiertechniken
373
' die alphabetisch ersten zehn Farben For i = 0 To 9 col = CType(sl.GetByIndex(i), Drawing.Color) colorName = CStr(sl.GetKey(i)) Console.WriteLine("Farbe {0}: R/G/B = {1}/{2}/{3}", _ colorName, col.R, col.G, col.B) Next End Sub
Im Konsolenfenster erscheint das folgende Ergebnis: sl enthält 167 Farben Farbe red: R/G/B = 255/0/0 Farbe activeborder: R/G/B = 212/208/200 Farbe activecaption: R/G/B = 10/36/106 Farbe activecaptiontext: R/G/B = 255/255/255 Farbe aliceblue: R/G/B = 240/248/255 Farbe antiquewhite: R/G/B = 250/235/215 Farbe appworkspace: R/G/B = 255/255/255 Farbe aqua: R/G/B = 0/255/255 ...
9.3.4
Datenaustausch zwischen Aufzählungsobjekten
Es gibt verschiedene Wege, um Daten von einer Aufzählung in eine andere oder in ein Feld zu kopieren bzw. um neue Zugriffsformen auf einmal gespeicherte Daten zu erlangen. Dieser Abschnitt stellt die wichtigsten dieser Wege vor.
Daten in ein Feld kopieren Fast alle Aufzählungsklassen implementieren die ICollection-Schnittstelle. Damit steht die Methode CopyTo zur Verfügung, mit der Sie die Elemente aus einer Aufzählung in ein Feld kopieren können. In der einfachsten Form kopiert CopyTo alle Elemente in ein Feld, das vorher entsprechend groß dimensioniert werden muss. Als Parameter müssen ein ausreichend großes Feld und ein Startindex (meist 0) angegeben werden. Manche Klassen stellen darüber hinaus mehrere CopyTo-Varianten zur Verfügung, mit denen die Anzahl der zu kopierenden Elemente limitiert werden kann. Dim al As New ArrayList() al.Add ... Dim obj(al.Count - 1) As Object al.CopyTo(obj, 0)
Beim Kopieren versucht CopyTo, die Objekte automatisch in den Datentyp des Felds zu kopieren. Wenn das nicht gelingt, kommt es zu einem Fehler. (Wenn sich die Ausgangsdaten in einer Hashtable befinden, werden DictionaryEntry-Objekte kopiert. Das Feld muss daher mit Object oder mit DictionaryEntry deklariert werden.) Wenn die Elemente der Auf-
374
9 Aufzählungen (Arrays, Collections)
zählung auf Referenztypen verweisen, werden nicht die Objekte selbst kopiert, sondern die Verweise auf die Objekte. (Das ist ein so genanntes shallow copy.)
Daten in eine Aufzählung kopieren Auch für den Datentransport in die umgekehrte Richtung sind bei den meisten Klassen Methoden vorgesehen. Bei einer Reihe von Collections-Objekten kann an New ein ICollectionObjekt übergeben werden. Damit werden die so angegebenen Elemente bereits bei der Erzeugung der Aufzählungsobjekts dorthin kopiert. Dim s() As String = {"a", "bB", "ccCC", "d"} Dim al As New ArrayList(s)
Mit der Methode AddRange können weitere Elemente eingefügt werden, die abermals aus einem ICollection-Objekt (also z.B. aus einem Feld) stammen. al.AddRange(s)
Unveränderliche ArrayList-Objekte erzeugen Die ArrayList-Methoden FixedSize und ReadOnly liefern in ihrer Größe bzw. in ihrem Inhalt unveränderliche ArrayList- bzw. IList-Objekte. Die Methoden werden überwiegend dann verwendet, wenn Sie in einer eigenen Funktion oder Methode eine Aufzählung als Ergebnis zurückgeben, aber vermeiden möchten, dass der Adressat der Daten diese verändert. ' al ist ein ArrayList-Objekt Dim readonly_list As ArrayList readonly_list = ArrayList.ReadOnly(al)
Die Ausgangsdaten, die ebenfalls in Form eines ArrayList- oder IList-Objekts vorliegen müssen, werden dabei nicht kopiert. Stattdessen wird eine neue Zwischenschicht (ein so genannter wrapper) zum Zugriff auf die Daten erzeugt. Wenn Sie nun via readonly_list auf die Aufzählung zugreifen, können Sie weder Elemente hinzufügen oder löschen, noch können Sie Objekte ändern. Hingegen ist die Aufzählung via al sehr wohl noch veränderlich! Wenn Sie also al.Remove("a") ausführen, verschwindet das Objekt sowohl aus al als auch aus readonly_list!
ArrayList-Adapter Mit der Methode ArrayList.Adapter können Sie um ein beliebiges IList-Objekt eine ArrayListVerwaltungsschicht legen. Das bedeutet, dass Sie alle ArrayList-Methoden auf das IList-Objekt anwenden können, soweit das Objekt dies zulässt. (Hier liegt eine wesentliche Grenze des Verfahrens: Sie können zwar mit ArrayList.Adapter ein ArrayList-Objekt aus einem gewöhnliches Feld bilden, die ArrayList-Methoden Add und Remove bleiben Ihnen aber weiterhin verwehrt, weil Felder als IList-Objekte mit FixedSize=True realisiert sind. Die Elementzahl kann also auch über die ArrayList-Schicht nicht verändert werden.)
9.3 Programmiertechniken
375
Das in Abbildung 9.1 dargestellte Beispielprogramm ist ein Vorgriff auf die Kapitel zur Windows-Programmierung. Es verwendet das in Abschnitt 14.6.1 vorgestellte ListBoxSteuerelement, mit dem eine Liste in einem Fenster angezeigt werden kann. Dieses Steuerelement bietet zwar die Möglichkeit, seine Listenelemente alphabetisch zu sortieren, Sie haben aber keinen Einfluss auf die dabei eingesetzte Vergleichsmethode. (Sie können also kein eigenes IComparer-Objekt angeben.) Diese Einschränkung kann mit einem ArrayListAdapter umgangen werden.
Abbildung 9.1: Die Elemente des Listenfelds wurden mit ArrayList.Sort sortiert
Der Programmcode ist nur verständlich, wenn Sie schon einmal ein Windows-Programm gesehen haben – aber unter dieser Voraussetzung sollte es keine Verständnisprobleme geben. Die Programmausführung beginnt (nach einigen Initialisierungsarbeiten) in Form_Load. Dort werden in das Listenfeld einige Namen eingefügt. Außerdem wird mit ArrayList.Adapter ein ArrayList-Objekt erzeugt, das auf die Liste verweist. Durch das Anklicken des Buttons wird die Prozedur Button1_Click ausgeführt. Dort wird die Sort-Methode auf das ArrayList-Objekt angewendet. Als Parameter wird ein Objekt der Klasse CompareByLastName übergeben. Damit werden die Listeneinträge nicht einfach alphabetisch sortiert, sondern unter Anwendung der Familiennamen. ' Beispiel aufzaehlungen\arraylist_adapter Dim al As ArrayList Private Sub Form1_Load(...) Handles MyBase.Load ListBox1.Items.AddRange(New String() _ {"Holger Schwichtenberg", ... "Andrew Troelsen"}) al = ArrayList.Adapter(ListBox1.Items) End Sub Private Sub Button1_Click(...) Handles Button1.Click al.Sort(New CompareByLastName()) End Sub Class CompareByLastName ... wie in Abschnitt 9.3.2
376
9 Aufzählungen (Arrays, Collections)
VERWEIS
9.3.5
Multithreading
Dieser Abschnitt setzt voraus, dass Sie sich schon ein wenig in das Thema Multithreading eingelesen haben. Multithreading bezeichnet die nahezu gleichzeitige Ausführung mehrere Programmteile in eigenen Teilprozessen (Thread). Weitere Informationen finden Sie in Abschnitt 12.6.
Wenn Sie in Multithreading-Anwendungen auf Collections-Objekte zugreifen, tritt das Problem auf, dass der Datenzugriff und insbesondere die Veränderung von Daten nicht Thread-sicher ist. Wenn ein Thread die Aufzählung verändert, während in einem zweiten Thread eine Schleife durchlaufen wird, kommt es zu einem Fehler. Die Prozedur threading_error demonstriert diesen Fehler. Im Start-Thread des Programms wird mit Do-Loop eine Endlosschleife ausgeführt. Innerhalb dieser Schleife werden mit ForEach alle Elemente der Aufzählung al durchlaufen. Die Endlosschleife wird circa zwei Mal pro Sekunde durch den Aufruf der Prozedur addItemEverySecond unterbrochen. Diese Prozedur wird in einem eigenen Thread ausgeführt und fügt eine Zufallszahl zwischen 0 und 1000 in die Aufzählung ein. Das Programm endet nach ein paar Sekunden mit dem Fehler InvalidOperationException, der dadurch ausgelöst wird, dass das ArrayList verändert wird, während die For-Each-Schleife ausgeführt wird. ' Beispiel aufzaehlungen\multi-threading Dim al As New ArrayList() Sub threading_error() ' addItemEverySecond alle 200 ms aufrufen Dim timerDelegate As New Threading.TimerCallback( _ AddressOf addItemEverySecond) Dim tm As New Threading.Timer(timerDelegate, Nothing, 0, 200) Dim obj As Object Do For Each obj In al Console.Write("{0} ", obj) Next Console.WriteLine() Loop End Sub Sub addItemEverySecond(ByVal status As Object) al.Add(CInt(Rnd() * 1000)) End Sub
9.3 Programmiertechniken
377
SyncRoot-Eigenschaft Zum Glück ist es einfach, diesen Fehler zu vermeiden: Sie müssen nur jeden Zugriff auf die Aufzählung, der eventuell problematisch sein könnte, synchronisieren. Dazu kapseln Sie sowohl die For-Each-Schleife als auch die Add-Methode durch die SyncLock-Anweisung. Als Parameter geben Sie dabei die SyncRoot-Eigenschaft an, die bei allen von ICollection abgeleiteten Klassen zur Verfügung steht. SyncRoot liefert ein Objekt, das explizit zur Synchronisierung von Aufzählungen vorgesehen ist. Um zu vermeiden, dass diese neue Version des Programms nun endlos läuft, wird das Programm beendet, wenn die Aufzählung mehr als 15 Elemente enthält. Natürlich hat diese Vorgehensweise auch einen Nachteil: Die Synchronisierung verlangsamt den Zugriff der beiden Threads auf die Aufzählung. Wenn die Aufzählung gerade durch einen Thread blockiert ist, muss der andere Thread auf das Ende der Blockade warten. Sub threading_without_error() ' addItemEverySecond alle 200 ms aufrufen Dim timerDelegate As New Threading.TimerCallback( _ AddressOf addItemEverySecondThreadSafe) Dim tm As New Threading.Timer(timerDelegate, Nothing, 0, 200) Dim obj As Object Do SyncLock al.SyncRoot For Each obj In al Console.Write("{0} ", obj) Next End SyncLock Console.WriteLine() If al.Count > 15 Then tm.Change(0, Threading.Timeout.Infinite) 'Timer-Thread tm.Dispose() 'abschalten Console.WriteLine("Return drücken") Console.ReadLine() Exit Sub 'Programmende End If Loop End Sub Sub addItemEverySecondThreadSafe(ByVal status As Object) SyncLock al.SyncRoot al.Add(CInt(Rnd() * 1000)) End SyncLock End Sub
378
9 Aufzählungen (Arrays, Collections)
9.4
Syntaxzusammenfassung
9.4.1
Schnittstellen
IEnumerable- und IEnumerator-Schnittstelle Zur Anwendung der Schnittstellen IEnumerable und IEnumerator formulieren Sie einfach eine For-Each-Schleife für das Aufzählungsobjekt. Mit den Interna dieser Schnittstellen müssen Sie sich nur dann beschäftigen, wenn Sie selbst Klassen programmieren möchten, die diese Schnittstelle realisieren. Collections.IEnumerable-Schnittstelle GetEnumerator()
liefert ein Objekt mit IEnumerator-Schnittstelle.
Collections.IEnumerator-Schnittstelle Current
verweist auf das aktuelle Objekt (Typ Object).
MoveNext()
geht weiter zum nächsten Objekt. Liefert False, wenn das Ende der Aufzählung erreicht ist.
Reset()
geht zurück an den Start.
Collections.IDictionaryEnumerator-Schnittstelle Entry
verweist auf das aktuelle Objekt (Typ Collections.DictionaryEntry).
Key
verweist auf den aktuellen Schlüssel (Object).
Value
verweist auf die aktuellen Daten (Object).
ICollection-Schnittstelle Collections.ICollection-Schnittstelle CopyTo(f, n)
kopiert die Elemente der Aufzählung in ein eindimensionales Feld beginnend mit f(n).
Count
liefert die Anzahl der Elemente in der Aufzählung.
IsSynchronized
gibt an, ob die Aufzählung synchroniziert ist. (Das bedeutet, dass sichergestellt ist, dass kein anderer Thread die Daten ändert.) IsSynchronized liefert bei fast allen Collection-Objekten False.
SyncRoot
liefert ein Objekt, das (mit SyncLock aufzählung.SyncRoot) zur Synchronisierung der Aufzählung verwendet werden kann.
9.4 Syntaxzusammenfassung
379
IList- und IDictionary-Schnittstelle Beachten Sie, dass sämtliche Methoden der Schnittstellen IList und IDictionary optional sind. Sie können sich also nicht darauf verlassen, dass Sie bei einer Klasse mit diesen Schnittstellen tatsächlich eine bestimmte Methode ausführen können. Im Regelfall geben IsFixedSize und IsReadOnly Auskunft über die Merkmale der Klasse, aber selbst diese beiden Eigenschaften sind optional. (Eigentlich führen diese Schnittstellen die Idee von Schnittstellen ad absurdum. Eine Schnittstelle ist ja ein Vertrag, bestimmte Methoden zur Verfügung zu stellen. Wenn aber alle Regeln des Vertrags mit der Klausel "wenn xy implementiert ist, dann ..." beginnen, fragt man sich, wozu es überhaupt einen Vertrag gibt.) Die Eigenschaft collobj.Item(x) verwenden Sie normalerweise nicht explizit. Stattdessen ist in VB.NET wie bei Feldern die Kurzschreibweise collobj(x) üblich. Collections.IList-Schnittstelle Add(obj)
fügt ein Objekt ein.
Clear()
löscht alle Elemente der Aufzählung.
Contains(obj)
testet, ob das Objekt bereits in der Aufzählung enthalten ist.
IndexOf(obj)
ermittelt den Index des Objekts. Liefert -1, wenn das Objekt nicht enthalten ist.
IsFixedSize
gibt an, ob die Anzahl der Elemente der Aufzählung unveränderlich ist. In diesem Fall stehen Add und Remove[At] nicht zur Verfügung.
IsReadOnly
gibt an, ob einzelne Elemente der Aufzählung verändert werden dürfen.
Item(n)
liefert das Objekt an der Indexposition n.
RemoveAt(n)
entfernt das Objekt an der Indexposition n aus der Aufzählung.
Remove(obj)
entfernt das angegebene Objekt aus der Aufzählung.
Collections.IDictionary-Schnittstelle Clear, Contains, IsFixedSize, IsReadOnly, Remove
funktionieren wie bei IList.
Add(keyobj, dataobj)
fügt das Objekt dataobj mit dem Schlüssel keyobj in die Aufzählung ein.
GetEnumerator()
liefert ein Objekt mit IDictionaryEnumerator-Schnittstelle. In For-Each-Schleifen werden DictionaryEntry-Objekte durchlaufen.
Item(keyobj)
liefert das Datenobjekt zum Schlüssel keyobj.
Keys
liefert ein ICollection-Objekt mit allen Schlüsselobjekten.
Values
liefert ein ICollection-Objekt mit allen Datenobjekten.
380
9.4.2
9 Aufzählungen (Arrays, Collections)
Klassen
Die folgende Tabelle gibt einen Überblick über die wichtigsten Klassen und ihre Merkmale. Schlüssel Daten Datentyp eindeutig sortieren Datentyp einfügen sortieren Array
Integer
+
beliebig
ArrayList
Integer
Object
StringCollection
Integer
+ +
Hashtable
Object
ListDictionary
Object
+ +
Object Object
+ +
HybridDictionary
Object
+
Object
+
StringDictionary
String
+
String
NameValueCollection
String
String
+ +
SortedList
Object / Integer
+
Object
+
CollectionBase
Integer
+
beliebig
+
ReadOnlyCollectionBase
Integer
beliebig
DictionaryBase
beliebig
+ +
NameObjectCollectionBase
String / Integer
+
String
+
+
+ + +
beliebig
+
beliebig
+
+
(+)
ArrayList-Klasse Die Klasse ArrayList realisiert die Schnittstellen IEnumerable, ICollection und IList. Die folgende Tabelle beschreibt nur solche Eigenschaften bzw. Methoden, die nicht ohnedies durch die Schnittstellen vorgegeben sind. Beim Erzeugen eines neuen Objekts durch New kann optional die voraussichtliche Anzahl der Aufzählungselemente angegeben werden. Collections.ArrayList-Klasse BinarySearch
ermöglicht eine sehr effiziente Suche nach Elementen, wenn die Aufzählung vorher sortiert wird.
Capacity
gibt an, wie viele Elemente die Aufzählung enthalten kann, bevor sie automatisch vergrößert wird.
Count
gibt an, wie viele Elemente die Aufzählung tatsächlich enthält.
GetRange
liefert eine Teilliste der Aufzählung.
9.4 Syntaxzusammenfassung
381
Collections.ArrayList-Klasse InsertRange
fügt mehrere Elemente, die als ICollection-Objekt angeben werden, in die Aufzählung ein.
LastIndex
sucht das letzte Element der Aufzählung, das ein bestimmtes Objekts enthält bzw. darauf verweist.
RemoveRange
entfernt mehrere Elemente gleichzeitig.
ArrayList.Repeat
liefert ein ArrayList-Objekt, das ein angegebenes Objekt n-Mal enthält.
Reverse
dreht die Reihenfolge der Elemente der Liste um.
Sort
sortiert die Elemente (optional unter Angabe eines IComparer-Objekts zur Durchführung des Objektvergleichs).
Synchronized
liefert eine synchronisierte Version des ArrayList-Objekts.
ToArray
kopiert die Elemente in ein Feld.
TrimToSize
gibt nicht benötigten Speicher für weitere Elemente frei.
Hashtable-Klasse Die Klasse Hashtable realisiert die Schnittstellen IEnumerable, ICollection und IDictionary. Darüber hinaus gibt es nur wenige wichtige Methoden. Collections.Hashtable-Klasse Contains
testet, ob das angegebene Objekt in der Aufzählung gespeichert ist.
ContainsKey
testet, ob der angegebene Schlüssel bereits in Verwendung ist.
SortedList-Klasse Die Klasse SortedList unterstützt unter anderem die Schnittstellen IEnumerable, ICollection und IDictionary. Die folgende Tabelle beschreibt die wichtigsten Methoden, die darüber hinausgehen. Collections.SortedList-Klasse GetByIndex
liefert das Objekt zur angegebenen Indexnummer.
GetKey
liefert den Schlüssel zur Indexnummer.
GetKeyList
liefert ein IList-Objekt mit allen Schlüsseln.
GetValueList
liefert ein IList-Objekt mit allen Datenelementen.
IndexOfKey
liefert die Indexnummer zum angegebenen Schlüssel.
382
9 Aufzählungen (Arrays, Collections)
Collections.SortedList-Klasse IndexOfValue
liefert die Indexnummer des ersten Elements, das das angegebene Objekt enthält.
RemoveAt
entfernt das Objekt mit der angegebenen Indexnummer.
10 Dateien und Verzeichnisse Im Mittelpunkt dieses Kapitels steht der .NET-Namensraum System.IO. Er enthält zahlreiche Klassen, mit denen Sie komfortabel den Verzeichnisbaum durchlaufen, Verzeichnisse erstellen und löschen sowie Dateien in verschiedenen Formaten (binär, Unicode-Text, ASCII-Text) lesen und verändern können. Das Kapitel geht auch auf einige IO-Besonderheiten ein, etwa auf asynchrone Dateioperationen, auf die Speicherung (Serialisierung) von Objekten und auf die Überwachung des Dateisystems. 10.1 10.2 10.3 10.4 10.5 10.6 10.7 10.8 10.9 10.10
Einführung und Überblick Klassen des System.IO-Namensraums Laufwerke, Verzeichnisse, Dateien Standarddialoge Textdateien lesen und schreiben Binärdateien lesen und schreiben Asynchroner Zugriff auf Dateien Verzeichnis überwachen Serialisierung IO-Fehler
384 386 389 416 421 435 449 456 458 468
384
10.1
10 Dateien und Verzeichnisse
Einführung und Überblick
Dieser Abschnitt gibt einen ersten Überblick über die zahlreichen Varianten (Bibliotheken, Klassen, Steuerelemente), die beim Umgang mit Dateien und Verzeichnissen helfen.
Kommandos und Bibliotheken zum Zugriff auf Dateien •
VB-Kommandos: Am Anfang (und damit meine ich Visual Basic seit Version 1!) gab es zum Zugriff auf Dateien und Verzeichnisse eine Reihe von an sich eigenständigen Kommandos wie CurDir, ChDir, FileCopy, Open, Read und Write. Das Konzept dieser Kommandos erinnert vielfach an die aus DOS-Zeiten vertrauten Kommandos. Diese Kommandos stehen aus Kompatibilitätsgründen innerhalb der Klasse Microsoft.VisualBasic.FileSystem noch immer zur Verfügung. (Falls Sie Get und Put vermissen: diese beiden Kommandos wurden in FileGet und FilePut umbenannt.) Der vielleicht einzige Grund, diese Kommandos noch immer einzusetzen, sind so genannte Random-Access-Dateien. Das sind Dateien, in denen Datensätze mit vorgegebener Länge relativ effizient gespeichert werden können. Allerdings empfiehlt Microsoft schon seit Jahren und mit vielen guten Gründen, derartige Daten in einer Datenbank zu speichern.
•
FSO (File Scripting Objects): In VB6 propagierte Microsoft die FSO-Bibliothek als Lösung aller Probleme, die im Umfeld des Dateizugriffs auftreten können. Tatsächlich bot die FSO-Bibliothek einen eleganten Zugriff auf das Dateisystem. Der größte Mangel bestand darin, dass Binärdateien weder ordentlich ausgelesen noch selbst erzeugt werden konnten. Die FSO-Bibliothek (Microsoft Scripting Runtime, Datei scrrun.dll) kann natürlich auch in VB.NET weiterhin benutzt werden. Allerdings handelt es sich um eine COM-Bibliothek, d.h., Sie nehmen mit der Nutzung den Overhead in Kauf, der durch die Kompatibilitätsschicht zwischen .NET und COM entsteht. Außerdem bietet die FSO-Bibliothek keine Vorteile gegenüber System.IO, wenn man mal davon absieht, dass bereits erworbenes Know-how weiter genutzt werden kann.
•
System.IO: Im Mittelpunkt dieses Kapitels steht die aktuellste Lösung, die Microsoft für den Zugriff auf Dateien anbietet, nämlich die System.IO-Klassenbibliothek. System.IO ist gewissermaßen der Nachfolger der FSO-Bibliothek. Gegenüber der FSO-Bibliothek bietet System.IO eine Menge Neuerungen: den Binärzugriff auf Dateien, Ereignisse zur Überwachung der Dateiaktivität, die Unterscheidung zwischen synchronen (Threadsicheren) und asynchronen Operationen, die Unterstützung verschiedener Codierungen (insbesondere Unicode UTF8) etc. Genau genommen ist System.IO keine eigenständige Bibliothek, sondern ein Bestandteil (Namensraum) von zwei Bibliotheken. Die meisten Objekte sind in der mscorlib-Bibliothek (Datei mscorlib.dll) enthalten, einige weniger oft benötigte Objekte in der systemBibliothek (Datei System.dll).
10.1 Einführung und Überblick
385
Das System.IO-Objektmodell ist grundsätzlich inkompatibel zur FSO-Bibliothek (auch wenn es einige Ähnlichkeiten gibt). FSO-Erfahrung hilft also beim Erlernen des System.IO-Objektmodells, erspart aber nicht einen gewissen Einarbeitungsaufwand. •
VERWEIS
Zugriff auf Active-Directory-Objekte: System.IO hilft nur beim Zugriff auf ein herkömmliches Dateisystem. Wenn Sie auf Objekte in einem Active Directory oder einem anderen Verzeichnisdienst (LDAP, IIS, NDS) zugreifen möchten, können Sie dazu die System.DirectoryServices-Bibliothek zu Hilfe nehmen (die in diesem Buch aber nicht beschrieben wird). Nicht behandelt werden in diesem Buch auch Klassen zum Lesen und Schreiben von XML-Dateien. Sie finden diese Klassen im Namensraum System.XML. XML wird in meinem zweiten VB.NET-Buch zu Datenbanken und Internet ausführlich beschrieben.
Steuerelemente und Komponenten •
OpenFileDialog und SaveFileDialog: Genau genommen handelt es sich hierbei nicht um
gewöhnliche Steuerelemente, sondern um im Formular unsichtbare Objekte, mit deren Methoden Sie einen eigenständigen Dialog zur Dateiauswahl anzeigen können. Die Dialogklassen sind Teil der System.Windows.Forms-Bibliothek. Ihre Anwendung wird in Abschnitt 10.4.1 beschrieben. •
FileSystemWatcher: Auch hierbei handelt es sich nicht wirklich um ein Steuerelement, sondern um eine ganz gewöhnliche Klasse aus dem System.IO-Namensraum, die bei Windows-Anwendungen über die Toolbox (Gruppe Komponenten) in das Programm eingefügt werden kann. Ein FileSystemWatcher-Objekt ermöglicht es, Änderungen im Dateisystem sehr einfach zu verfolgen. Es löst dazu bei jeder Veränderung ein Ereignis aus. Informationen zur Anwendung des Steuerelements finden Sie in Abschnitt 10.8.
•
DirectoryEntry und DirectorySearcher: Diese beiden Klassen sind ebenfalls über die Toolbox (Gruppe Komponenten) zugänglich. Sie helfen dabei, auf Objekte in einem Active Directory zuzugreifen bzw. diese zu finden. Die Steuerelemente sind Teil der System.DirectoryServices-Bibliothek, die in diesem Buch nicht beschrieben wird.
•
DriveListBox, DirListBox und FileListBox: Die seit VB1 vertrauten Steuerelemente zei-
gen in Listenfeldern alle Laufwerke des Rechners, deren Verzeichnisse und Dateien an. Diese Steuerelemente werden unter .NET nicht mehr offiziell unterstützt und daher auch in der Toolbox nicht angezeigt. Mit TOOLBOX ANPASSEN können Sie die Steuerelemente aber manuell aktivieren (Dialogblatt .NET-FRAMEWORK-KOMPONENTEN, AssemblyName Microsoft.VisualBasic.Compatibility). In diesem Buch wird auf eine Beschreibung verzichtet.
386
10 Dateien und Verzeichnisse
10.2
Klassen des System.IO-Namensraums
HINWEIS
Dieser Abschnitt gibt einen Überblick über die System.IO-Klassen und ihre Anwendung. Im Detail werden die meisten Klassen dann im Verlauf der weiteren Abschnitte vorgestellt. Wenn Sie sich die System.IO-Klassen im Objektbrowser ansehen, beachten Sie bitte, dass die Klassen auf zwei gleichnamige Namensräume verteilt sind: System.IO in mscorlib (Datei mscorlib.dll) sowie System.IO in system (also der system-Klassenbibliothek System.dll).
Dateien und Verzeichnisse bearbeiten IO.File und IO.Directory: Diese Klassen enthalten Methoden, um Dateien bzw. Verzeichnisse direkt zu bearbeiten, ohne vorher Objekte zu erzeugen. Beispielsweise können Sie mit IO.File.Move("C:\name1", "C:\name2") eine Datei umbenennen. IO.FileInfo und IO.DirectoryInfo: Die von diesen Klassen abgeleiteten Objekte dienen in
erster Linie dazu, Informationen über Dateien und Verzeichnisse zu ermitteln (Größe, Datum der letzten Änderung, alle Dateien innerhalb eines Verzeichnisses etc.). Sie können die Objekte aber auch direkt bearbeiten (kopieren, löschen etc.). IO.Path: Diese Klasse enthält eine Reihe von Methoden, die bei der Manipulation von Datei- und Verzeichnisnamen helfen. Beispielsweise können Sie mit GetPathRoot das Laufwerk eines Verzeichnispfades extrahieren, mit Combine zwei Verzeichnisse aneinanderfügen etc. GetTempPath und GetTempFileName liefern das temporäre Verzeichnis bzw.
einen Dateinamen für eine temporäre Datei.
Dateien lesen und schreiben IO.StreamReader und IO.StreamWriter: Mit diesen Klassen können Sie Textdateien komfortabel lesen und schreiben. Per Default gilt dabei das Unicode-UTF8-Format, es kann aber auch eine andere Codierung gewählt werden. Der Dateizugriff ist rein sequentiell, es ist daher nicht möglich, die Schreib- oder Leseposition unmittelbar zu beeinflussen. IO.FileStream: Diese Klasse ermöglicht den Low-Level-Zugriff auf Dateien. Sie können damit einzelne Bytes lesen bzw. (über-)schreiben. Dabei können Sie die Lese- bzw. Schreibposition (den Dateizeiger) jederzeit verändern. Die Lese- und Schreiboperationen können wahlweise synchron oder asynchron erfolgen. IO.BinaryReader und IO.BinaryWriter: Diese Klassen ermöglichen wie FileStream den Binärzugriff auf Dateien. Allerdings unterstützen die Read- und Write-Methoden die meisten
.NET-Datentypen, so dass Sie sich nicht um jedes einzelne Byte zu kümmern brauchen.
10.2 Klassen des System.IO-Namensraums
387
Dateisystem überwachen IO.FilesystemWatcher: Mit dieser Klasse können Sie ein Verzeichnis des Dateisystems über-
wachen (nur unter Windows NT/2000/XP). Jedes Mal, wenn innerhalb dieses Verzeichnisses eine Datei verändert wird, kommt es zum automatischen Aufruf einer Ereignisprozedur.
Fehlerbehandlung IO.*Exception: Diese Klassen beschreiben einige Fehler (Exceptions), die bei der Bearbeitung von System.IO-Objekten auftreten können. Neben diesen Fehlern gibt es aber noch weitere
Fehlermöglichkeiten (siehe auch Abschnitt 10.10).
Sonstige IO.FileSystemInfo: Die Klassen FileInfo und DirectoryInfo sind von FileSystemInfo abgeleitet.
Diese Klasse stellt einige gemeinsame Methoden und Eigenschaften zur Verfügung. IO.TextReader und IO.TextWriter: Das sind die Basisklassen für StringReader bzw. -Writer sowie StreamReader. Diese Klassen stellen einige gemeinsame Eigenschaften zur Verfügung, werden aber üblicherweise nicht direkt verwendet. IO.StringReader und IO.StringWriter: Ähnlich wie StreamReader und StreamWriter können Sie mit den Methoden Text lesen bzw. schreiben. Der wesentliche Unterschied besteht darin, dass als Datenquelle bzw. als Ziel nun Zeichenketten statt Dateien gelesen bzw. geschrieben werden. IO.Stream: Das ist die Basisklasse für FileStream, BufferedStream, MemoryStream sowie einige weitere Stream-Klassen außerhalb des System.IO-Namensraums. IO.BufferedStream: Diese Klasse optimiert wiederholte Lese- oder Schreibvorgänge aus bzw. in IO.Stream-Objekten. IO.MemoryStream: Diese Klasse funktioniert wie FileStream; allerdings wird der Datenstrom im Arbeitsspeicher verwaltet (statt in einer Datei).
Objekthierarchie im System.IO-Namensraum Die folgenden Diagramme zeigen, wie die Klassen voneinander abgeleitet sind. Die in diesem Kapitel beschriebenen System.IO-Klassen sind fett hervorgehoben.
388
10 Dateien und Verzeichnisse
Dateien und Verzeichnisse bearbeiten Object ├─ Directory ├─ File ├─ MarshalByRefObject
│ │ │ ├─ Component │ │ └─ FileSystemWatcher │ └─ FileSystemInfo │ ├─ DirectoryInfo │ └─ FileInfo └─ Path
.NET-Basisklasse Verzeichnis unmittelbar bearbeiten Datei unmittelbar bearbeiten Objekt nur als Referenz an andere Rechner weitergeben Basisklasse für Komponenten Dateisystem überwachen Basisklasse für DirectoryInfo und FileInfo Verzeichnisse ermitteln Informationen über Dateien ermitteln Hilfsfunktionen zur Ermittlung und Bearbeitung von Datei- und Verzeichnisnamen
Textdateien Object └─ MarshalByRefObject
│ ├─ TextReader │ ├─ StreamReader │ └─ StringReader └─ TextWriter ├─ StreamWriter └─ StringWriter
.NET-Basisklasse Objekt nur als Referenz an andere Rechner weitergeben Basisklasse für StreamReader und StringReader Textdateien lesen Informationen aus Zeichenkette lesen Basisklasse für StreamWriter und StringWriter Textdateien schreiben Informationen in Zeichenkette schreiben
Binärdateien Object ├─ BinaryReader ├─ BinaryWriter └─ MarshalByRefObject
│ └─ Stream ├─ FileStream ├─ BufferedStream │ └─ MemoryStream
.NET-Basisklasse .NET-Datentypen aus Binärdateien lesen .NET-Datentypen in Binärdateien speichern Objekt nur als Referenz an andere Rechner weitergeben Basisklasse für Buffered-, File- und MemoryStream Bytes in/aus Binärdateien speichern/lesen wiederholte Lese- oder Schreibzugriffe bei IO.Stream-Objekten optimieren wie FileStream, Daten werden aber im Arbeitsspeicher verwaltet
Von IO.Stream sind übrigens nicht nur die drei IO-Klassen BufferedStream, FileStream und MemoryStream abgeleitet, sondern auch Klassen anderer Namensräume (z.B. Net.Sockets.NetworkStream und Security.Cryptography.CryptoStream).
10.3 Laufwerke, Verzeichnisse, Dateien
10.3
389
Laufwerke, Verzeichnisse, Dateien
Im Mittelpunkt dieses Abschnitts stehen vier System.IO-Klassen: File: ermöglicht die Bearbeitung einer Datei (öffnen, kopieren, verschieben, löschen etc.). FileInfo: liefert genaue Informationen über eine Datei. Directory: ermöglicht die Bearbeitung eines Verzeichnisses. DirectoryInfo: liefert genaue Informationen über ein Verzeichnis.
HINWEIS
FileInfo und DirectorInfo sind beide von der Klasse FileSystemInfo abgeleitet. Diese
Klasse wird selten direkt verwendet, sie stellt aber diverse Eigenschaften und Methoden zur Verfügung, die sowohl für FileInfo als auch für DirectorInfo angewendet werden können. Beachten Sie, dass diese gemeinsamen Schlüsselwörter im Objektbrowser nicht bei den Klassen FileInfo und DirectorInfo aufscheinen, sondern bei FileSystemInfo!
File und FileInfo bzw. Directory und DirectoryInfo weisen ähnlich lautende Eigenschaften und
Methoden auf und scheinen sich auf den ersten Blick zu entsprechen. Das täuscht aber: FileInfo und DirectoryInfo dienen primär dazu, eine konkrete Datei bzw. ein konkretes Verzeichnis zu bearbeiten. Dazu muss vorher mit myfile = New IO.FileInfo("name") bzw. mit mydir = New IO.DirectoryInfo ein entsprechendes Objekt erzeugt werden. In der Folge kann dieses Objekt mit Methoden und Eigenschaften bearbeitet werden (z.B. myfile. Length, um die Größe der Datei zu ermitteln).
•
Die Methoden der File- bzw. Directory-Klassen können dagegen eingesetzt werden, ohne vorher ein Objekt zu erzeugen. (Es ist nicht einmal möglich, ein Objekt dieser Klassen zu erzeugen, d.h., New steht für File und Directory nicht zur Verfügung.) Beispielsweise können Sie eine Datei mit IO.File. Copy("alter name", "neuer name") kopieren.
VERWEIS
•
Man kann den Unterschied zwischen File/Directory und FileInfo/DirectoryInfo auch in der Nomenklatur der objektorientierten Programmierung ausdrücken: Alle Mitglieder der FileInfo bzw. DirectoryInfo sind so genannte Instance-Mitglieder, die erst dann verwendet werden können, wenn vorher ein Objekt erzeugt wurde. Die Mitglieder von File bzw. Directory sind dagegen Shared-Mitglieder, die immer zur Verfügung stehen. Hintergründe zur Unterscheidung zwischen Shared- und Instance-Mitgliedern finden Sie in Abschnitt 6.2.4.
Auch wenn die Namenserweiterung-Info auf einen inhaltlichen Unterschied zwischen den Klassen hinzuweisen scheint, ist der Unterschied rein formal. Inhaltlich überlappen sich die Klassen weitestgehend. Beispielsweise können Sie das Datum der letzten Änderung einer Datei sowohl mit IO.File.GetLastAccessTime("name") als auch mit myFileInfoObject. LastAccessTime ermitteln. Sie können eine Datei sowohl mit IO.File.Delete("name") als auch mit myFileInfoObject.Delete löschen.
390
10 Dateien und Verzeichnisse
10.3.1 Informationen über Verzeichnisse und Dateien ermitteln Als Ausgangspunkt für erste Experimente kann das aktuelle Verzeichnis dienen, das mit dem Namen "." angesprochen wird. Die folgende Zeile erzeugt daher ein DirectoryInfoObjekt dieses Verzeichnisses:
HINWEIS
Dim dir As IO.DirectoryInfo = New IO.DirectoryInfo(".")
Wenn ein Verzeichnis, dessen Namen Sie an New IO.DirectoryInfo übergeben, nicht existiert, wird der Code dennoch ohne Fehlermeldung ausgeführt und ein DirectoryInfo-Objekt erzeugt! Ob es das Verzeichnis tatsächlich gibt, müssen Sie mit dir.Exists testen. (Das Gleiche gilt auch für FileInfo-Objekte.) Falls Sie ein Verzeichnis neu erstellen möchten, sieht der Code so aus: Dim dir As IO.DirectoryInfo = _ IO.Directory.CreateDirectory("c:\test1")
Nun können Sie über das dir-Objekt verschiedene Eigenschaften des Verzeichnisses ermitteln, beispielsweise: dir.Name dir.FullName
HINWEIS
dir.LastWriteTime dir.Attributes
'Name des Verzeichnisses (z.B. "bin") 'vollständiger Name (z.B. ' "D:\vb.net\test\test2\bin") 'Zeitpunkt der letzten Änderung 'Eigenschaften (z.B. Directory, siehe unten)
Alle Eigenschaften und Methoden von System.IO erwarten bzw. liefern Verzeichnisse ohne ein abschließendes \-Zeichen! Ein Verzeichnis wird also korrekt in der Form "C:\verzeichnis" angegeben (nicht "C:\verzeichnis\"). Die einzige Ausnahme von dieser Regel ist das Wurzelverzeichnis eines Laufwerks: dieses wird mit einem abschließenden \-Zeichen angegeben, also "C:\". Falls Sie Code schreiben möchten, der nicht nur unter Windows, sondern auch unter anderen Betriebssystemen läuft, sollten Sie statt des Zeichens "\" die Konstante IO.Path.PathSeparator verwenden (siehe Abschnitt 10.3.5).
Ganz ähnlich sieht die Vorgehensweise aus, wenn Sie einige Eigenschaften einer Datei ermitteln möchten. Mit der folgenden Anweisung erzeugen Sie ein FileInfo-Objekt für die Datei C:\Winnt\Notepad.exe. (Wenn Sie das Beispiel ausprobieren, verwenden Sie den Namen einer existierenden Datei.) Dim fil As New IO.FileInfo("c:\winnt\notepad.exe")
Nun können Sie sich mit Exists vergewissern, ob die Datei tatsächlich existiert, mit Length die Größe der Datei (in Bytes) ermitteln etc.:
10.3 Laufwerke, Verzeichnisse, Dateien
ACHTUNG
fil.Exists fil.Length fil.Extension
391
'True, wenn die Datei existiert 'Größe der Datei (in Byte) 'Dateikennung, z.B. ".exe"
Viele Eigenschaften der DirectoryInfo- und FileInfo-Objekte sind statisch. Mit anderen Worten: Wenn dass Objekt erzeugt wird, werden Eigenschaften wie Exists, Length, Attributes etc. gelesen. Wenn sich die Datei oder das Verzeichnis auf der Festplatte nun ändert (z.B. weil die Datei gelöscht wird), bleiben die Eigenschaften unverändert! dir.Exists liefert noch immer True. Sie müssen die Methode Refresh ausführen, um diese Eigenschaften auf den aktuellen Stand zu bringen!
Datei- und Verzeichnisattribute Die Attributes-Eigenschaft der File- und DirectoryInfo-Objekte bedarf einer etwas ausführlicheren Erklärung: Sie liefert eine Kombination von FileAttributes-Konstanten zurück: Archive, Compressed, Device, Directory, Encrypted, Hidden etc. Attributes.ToString liefert eine Zeichenkette, in der die Attribute aneinandergereiht sind: s = fil.Attributes.ToString
'z.B. s = "ReadOnly, Archive"
Ob das Verzeichnis ein bestimmtes Merkmal hat, können Sie so testen: If (fil.Attributes And IO.FileAttributes.Compressed) 0 Then ...
Eine Liste mit allen Attributen finden Sie in der Syntaxzusammenfassung (Abschnitt 10.3.8).
10.3.2 Alle Dateien und Unterverzeichnisse verarbeiten Bis jetzt wurden nur solche Eigenschaften und Methoden von FileInfo bzw. DirectoryInfo verwendet, die Informationen unmittelbar zur jeweiligen Datei bzw. zum Verzeichnis liefern. Oft wollen Sie aber alle Dateien oder alle Unterverzeichnisse eines Verzeichnisses bearbeiten. Dabei helfen die Methoden GetFiles bzw. GetDirectories der DirectoryInfo-Klasse, die im Mittelpunkt dieses Abschnitts stehen.
Alle Dateien eines Verzeichnisses ermitteln Die Methode dir.GetFiles liefert ein Feld von FileInfo-Objekten des DirectoryInfo-Objektes dir. Die folgende Schleife schreibt die Namen aller Verzeichnisse der Festplatte C: in ein Konsolenfenster. Dim dir As New IO.DirectoryInfo("c:\") Dim fil As IO.FileInfo For Each fil In dir.GetFiles() Console.WriteLine(fil.FullName) Next
392
10 Dateien und Verzeichnisse
HINWEIS
Wenn Sie nicht alle Dateien ermitteln möchten, übergeben Sie an GetFiles einfach das gewünschte Suchmuster. Beispielsweise findet GetFiles("*.txt") alle .txt-Dateien im Verzeichnis. GetFiles bietet allerdings keine Möglichkeiten, nur solche Dateien zu ermitteln, die bestimmte Eigenschaften oder Attribute aufweisen (etwa alle schreibgeschützten Dateien). Durch GetFiles wird ein Feld erzeugt, das zum Zeitpunkt der Erzeugung den Zustand des Dateisystems wiederspiegelt. Das Feld ist aber statisch. Es kann also passieren, dass während der Zeit, in der Sie in einer Schleife das Feld verarbeiten, neue Dateien erzeugt bzw. Dateien gelöscht werden!
Dateien sortieren: Bei ersten Tests mit GetFiles – z.B. unter Windows 2000 mit NTFS-Dateisystem – kann man den Eindruck gewinnen, dass die Methode die Dateien grundsätzlich in sortierter Reihenfolge liefert (binärer Vergleich). Diese Eigenschaft von GetFiles ist aber nicht dokumentiert, und tatsächlich zeigen Tests mit einem Netzwerkverzeichnis, dass die Dateien dort plötzlich in willkürlicher Reihenfolge geliefert werden. Wenn Sie also eine sortierte Liste der Dateinamen benötigen, müssen Sie sich selbst um die Sortierung kümmern! Das folgende Beispiel demonstriert, wie Sie das am leichesten bewerkstelligen können: Sie verwenden zum Sortieren die Methode Array.Sort. Entscheidend ist, dass Sie an diese Methode eine Klasse übergeben müssen, die den Vergleich zwischen zwei Dateien durchführt. Diese Klasse muss die Schnittstelle Collections.ICompare und deren Methode Compare implementieren. (Hintergrundinformationen zu Array.Sort und der ICompare-Schnittstelle finden Sie in Abschnitt 9.3.2.) ' Beispiel dateien\dateien-sortieren Option Strict On Module Module1 Sub Main() Dim dir As New IO.DirectoryInfo("c:\") Dim files As IO.FileInfo() Dim fil As IO.FileInfo files = dir.GetFiles() 'sortieren Array.Sort(files, New myFileComparer()) 'im Konsolenfenster ausgeben For Each fil In files Console.WriteLine(fil.FullName) Next Console.WriteLine("Drücken Sie Return!") Console.ReadLine() End Sub
10.3 Laufwerke, Verzeichnisse, Dateien
393
' Klasse zum Dateinamenvergleich von FileInfo-Objekten Class myFileComparer Implements Collections.IComparer Overridable Function Compare( _ ByVal obj1 As Object, ByVal obj2 As Object) As Integer _ Implements IComparer.Compare Dim fil1, fil2 As IO.FileInfo fil1 = CType(obj1, IO.FileInfo) fil2 = CType(obj2, IO.FileInfo) Return String.Compare(fil1.Name, fil2.Name) End Function End Class End Module
Abbildung 10.1: Beispielprogramm für das Sortieren von Dateien
Alle Unterverzeichnisse eines Verzeichnisses ermitteln GetDirectories funktioniert exakt wie GetFiles, liefert aber ein Feld von DirectoryInfo-Elementen, mit deren Hilfe Sie auf alle Unterverzeichnisse zugreifen können.
Alternativen zu DirectoryInfo.GetFiles() und .GetDirectories() Anstatt die Dateien und Unterverzeichnisse eines Verzeichnisses getrennt zu ermitteln, können Sie auch beide Gruppen mit einer einzigen Methode der DirectoryInfo-Klasse ermitteln, nämlich mit GetFileSystemInfos. Sie erhalten damit als Ergebnis ein FileSystemInfoFeld. Die einzelnen Elemente des Felds sind aber FileInfo oder DirectoryInfo-Objekte. Den tatsächlichen Objekttyp können Sie mit TypeName oder .GetType.FullName feststellen. Sie können auch mit .Attributes testen, ob es sich um ein Verzeichnis handelt oder nicht. Statt der drei GetXxx-Methoden der DirectoryInfo-Klassen können Sie auch die Methoden GetFiles, GetDirectories und GetFileSystemEntries der Directory-Klasse verwenden. An diese Methoden müssen Sie den Pfad des Startverzeichnisses als Zeichenkette übergeben. Die Methoden liefern keine Objektfelder als Resultat, sondern Felder mit den Zeichenketten der Dateien bzw. Verzeichnisse.
394
10 Dateien und Verzeichnisse
Dim s As String For Each s In IO.Directory.GetFileSystemEntries("c:\") Console.WriteLine(s) Next
Verzeichnisbaum rekursiv durchlaufen Immer wieder kommt es vor, dass Sie alle Dateien eines ganzen Verzeichnisbaums verarbeiten möchten – sei es, dass Sie eine bestimmte Datei suchen, sei es, dass Sie alle Dateien verändern oder dass Sie den Gesamtplatzbedarf ermitteln möchten. Den besten Weg bietet hierfür eine rekursive Funktion, die – ausgehend von einem Startverzeichnis – für alle Unterverzeichnisse aufgerufen wird. Die folgenden Zeilen geben ein Muster für eine derartige Funktion. Sub processDirectory(ByVal dir As IO.DirectoryInfo) Dim subdir As IO.DirectoryInfo Dim file As IO.FileInfo ' Debug.WriteLine("Verzeichnis: " + dir.FullName) ' alle Dateien des aktuellen Verzeichnisses bearbeiten For Each file In dir.GetFiles() ... Code zur Bearbeitung der Dateien Next ' rekursiv alle Unterverzeichnisse bearbeiten For Each subdir In dir.GetDirectories() processDirectory(subdir) Next End Sub
Die Prozedur kann beispielsweise mit processDirectory(New IO.DirectoryInfo("C:\")) gestartet werden (um alle Dateien und Verzeichnisse von Laufwerk C: zu bearbeiten).
Beispielprogramm: Anzahl und Größe aller Dateien ermitteln Das folgende Programm basiert auf dem obigen Muster processDirectory. Das Programm ermittelt die Anzahl der Dateien und Verzeichnisse sowie den Gesamtplatzbedarf aller Dateien im Laufwerk C:. Außerdem wird für jedes Verzeichnis im Konsolenfenster angezeigt, wie viele Dateien und Verzeichnisse darin enthalten sind und wie groß der Platzbedarf der unmittelbar enthaltenen Dateien ist (siehe Abbildung 10.2). Beachten Sie, dass das Programm nicht den tatsächlichen Platzbedarf ermittelt, sondern einfach die Größe der Dateien addiert (FileInfo.Length). Der tatsächliche Platzbedarf ist in der Regel etwas größer, weil Dateien blockweise gespeichert werden, diese Blöcke aber nur dann vollständig gefüllt werden, wenn die Dateigröße zufällig mit der Blockgröße übereinstimmt. Außerdem beanspruchen auch Verzeichnisse etwas Platz auf der Festplatte. Andererseits kann der wahre Platzbedarf kleiner sein, wenn Dateien oder ganze Verzeichnisse komprimiert sind (nur bei einem NTFS-Dateisystem).
10.3 Laufwerke, Verzeichnisse, Dateien
395
Abbildung 10.2: Die ersten und die letzten Ergebniszeilen des Beispielprogramms verzeichnisbaum
Im folgenden Programmlisting wurde aus Platzgründen der Code für die Zeiterfassung entfernt (siehe Abschnitt 8.3.2). ' dateien\verzeichnisbaum Dim totalDirCount, totalFileCount, totalFileSize As Long Dim totalFileErrors, totalDirErrors As Long Sub Main() processDirectory(New IO.DirectoryInfo("C:\")) ' Zusammenfassung Console.WriteLine() Console.WriteLine("Anzahl der Verzeichnisse: {0}", totalDirCount) Console.WriteLine("Anzahl der Dateien: {0}", totalFileCount) Console.WriteLine("Gesamtgröße aller Dateien: {0:d} MB", _ CInt(totalFileSize / 1024 ^ 2)) Console.WriteLine("Verzeichnisfehler: {0}", totalDirErrors) Console.WriteLine("Dateifehler: {0}", totalFileErrors) Console.WriteLine() Console.WriteLine("Drücken Sie Return!") Console.ReadLine() End Sub processDirectory ist im Vergleich zum obigen Muster vor allem durch die zweistufige Fehlerabsicherung aufgebläht: Der erste Try-Block für die Schleife For Each file In dir.GetFiles() kommt dann zur Geltung, wenn der Zugriff auf ein ganzes Verzeichnis nicht möglich ist. Die wahrscheinlichste Ursache sind fehlende Zugriffsrechte (insbesondere dann, wenn Sie das Programm nicht mit Administrator-Rechten ausführen).
396
10 Dateien und Verzeichnisse
Der zweite Try-Block dient speziell für die Anweisung fileSize += file.Length. Auch hier ist die wahrscheinlichste Fehlerursache, dass Sie auf die betroffene Datei nicht zugreifen dürfen (und daher nicht einmal seine Größe ermitteln können). Vereinzelt kann aber auch eine IO.PathTooLongException die Fehlerursache sein (also ein zu langer Dateiname).
VERWEIS
Falls Fehler auftreten, werden diese mit Debug.WriteLine dokumentiert. (Sie können sich die Fehlermeldungen im der Entwicklungsumgebung im Ausgabefenster ansehen.) Einführende Informationen zur Fehlerabsicherung von Dateizugriffen finden Sie in Abschnitt 10.8. Eine allgemeine Einführung in die Fehlerabsicherung mit VB.NET (also eine Beschreibung des Try-Catch-Konzepts) gibt Abschnitt 11.1.
Ansonsten sollte der Code auf Anhieb verständlich sein. Die Variablen fileSize, fileCount und dirCount dienen zum Sammeln der Informationen über das aktuelle Verzeichnis. Die Variable indent misst die Rekursionstiefe von processDirectory. Diese Information wird für die Ausgabe der Zeilen im Konsolenfenster ausgewertet (so dass die Verzeichnisse entsprechend der Verzeichnisstruktur eingerückt sind, siehe Abbildung 10.2). Sub processDirectory(ByVal dir As IO.DirectoryInfo) Dim fileSize, fileCount, dirCount As Long Static indent As Integer Dim subdir As IO.DirectoryInfo Dim file As IO.FileInfo ' Einrückung für Konsolenausgabe indent += 1 Try ' wenn bereits in der nächsten Zeile ein Fehler auftritt, ' kann wahrscheinlich auf das gesamte Verzeichnis nicht ' zugegriffen werden (z.B. UnauthorizedAccessException) For Each file In dir.GetFiles() ' wenn in den beiden folgenden Zeilen ein Fehler auftritt, ' kann auf eine einzelne Datei nicht zugegriffen werden Try fileCount += 1 fileSize += file.Length Catch e As Exception ' Problem beim Zugriff auf eine Datei totalFileErrors += 1 Debug.WriteLine("Fehler: " + TypeName(e) + ": " + e.Message) Debug.WriteLine(" In Verzeichnis: " + dir.FullName) Debug.WriteLine(" Bei Datei: " + file.Name) End Try Next
10.3 Laufwerke, Verzeichnisse, Dateien
397
' Informationen über das aktuelle Verzeichnis angeben Console.WriteLine( _ "{0}{1}: {2} Verz., {3} Dateien, Platzbedarf {4} kB", _ Space((indent - 1) * 2), dir.Name, _ dir.GetDirectories().Length, fileCount, CInt(fileSize / 1024)) ' rekursiv alle Unterverzeichnisse bearbeiten For Each subdir In dir.GetDirectories() dirCount += 1 processDirectory(subdir) Next Catch e As Exception ' Problem beim Zugriff auf ein Verzeichnis totalDirErrors += 1 Debug.WriteLine("Fehler: " + TypeName(e) + ": " + e.Message) Debug.WriteLine(" In Verzeichnis: " + _ IO.Path.Combine(dir.Parent.FullName, dir.Name)) End Try ' Gesamtergebnis addieren totalDirCount += dirCount totalFileCount += fileCount totalFileSize += fileSize ' Einrückung für Konsolenausgabe indent -= 1 End Sub
Erwarten Sie übrigens keine Geschwindigkeitswunder von dem Programm. Auf meinem Rechner erfordert das Durcharbeiten von C: (ca. 80000 Dateien, ca. 6 GByte Daten) beim ersten Durchlauf mehr als eine Minute. Wird das Programm anschließend nochmals gestartet, liefert es bereits nach etwa 15 Sekunden das Endergebnis. Der Grund für diese deutliche Geschwindigkeitssteigerung liegt darin, dass beim zweiten Mal fast alle Dateiinformationen im lokalen Zwischenspeicher des Betriebssystems liegen und daher nur vergleichsweise wenige Zugriffe auf die Festplatte erforderlich sind. (Beim zweiten Durchlauf beansprucht übrigens auch die Bildschirmausgabe einen nennenswerten Anteil der Rechenzeit. Wenn Sie darauf verzichten, wird das Programm nochmals erheblich schneller.)
10.3.3 Manipulation von Dateien und Verzeichnissen Bis jetzt beschränkten sich die Beispiele dieses Kapitels auf das Ermitteln von Informationen. Selbstverständlich können Sie aber Dateien und Verzeichnisse auch ändern, d.h. kopieren, verschieben, neu anlegen und wieder löschen. Wie bereits in der Einleitung dieses Kapitels ausgeführt wurde, bietet der System.IO-Namensraum dazu grundsätzlich zwei Möglichkeiten:
398
10 Dateien und Verzeichnisse
•
Sie können entweder ein Objekt der Klassen FileInfo oder DirectoryInfo erzeugen und dieses dann mit Methoden wie Delete oder Copy bearbeiten.
•
Oder Sie können die Methoden der Klassen File oder Directory verwenden, wobei Sie die zu bearbeitende Datei bzw. das Verzeichnis als Zeichenkette angeben.
Welche der beiden Varianten vorzuziehen ist, hängt primär davon ab, ob es aus dem bisherigen Code heraus bereits ein FileInfo- oder DirectoryInfo-Objekt gibt. Wenn das nicht der Fall ist, sind die File- oder Directory-Methoden vorzuziehen (klarerer und kompakterer Code). Ein weiteres Kriterium kann der Rückgabewert sein: So liefert File.Copy keinen Rückgabewert, während myFileInfoObject.Copy ein neues FileInfo-Objekt für die kopierte Datei zurückgibt. Die folgenden Absätze stellen exemplarisch Methoden aller vier Klassen vor. Eine vollständige Referenz finden Sie in der Syntaxzusammenfassung.
Neue Dateien und Verzeichnisse erzeugen Um ein neues Verzeichnis zu erzeugen, verwenden Sie am einfachsten IO.Directory.CreateDirectory. Falls das Verzeichnis schon existiert, wird es nicht verändert. Die Methode liefert ein DirectoryInfo-Objekt zurück: ' Beispielprogramm dateien/manipulation ' Verzeichnis erzeugen Dim dir As IO.DirectoryInfo = IO.Directory.CreateDirectory("c:\test1")
Wenn Sie das DirecotryInfo-Objekt nicht benötigen, können Sie den Rückgabewert einfach ignorieren und ersparen sich dann die Deklaration der Variablen dir. IO.Directory.CreateDirectory("c:\test1")
Ungleich mehr Möglichkeiten gibt es, eine Datei zu erzeugen – abhängig davon, was Sie in der Folge mit der Datei tun möchten: Create liefert als Rückgabewert ein FileStream-Objekt, das eine sehr vielseitige Nutzung der Datei (auch in Binärform) ermöglicht. CreateText liefert dagegen ein StreamWriter-Objekt, dessen Verwendung sich vor allem zum Schreiben von Textdateien anbietet. Schließlich können Sie noch Open verwenden und im Modusparameter IO.FileMode.Create, .CreateNew oder .OpenOrCreate angeben (damit nicht nur eine vorhandene Datei geöffnet, sondern gegebenenfalls auch eine neue erzeugt wird). Open liefert ein FileStream-Objekt. ' Dateien zur Bearbeitung erzeugen Dim fs1, fs2 As IO.FileStream Dim sw As IO.StreamWriter fs1 = IO.File.Create("c:\test1\test1.bin") fs2 = IO.File.Open("c:\test1\test2.bin", IO.FileMode.OpenOrCreate) sw = IO.File.CreateText("c:\test1\test3.txt")
Auch hier können Sie natürlich den Rückgabewert ignorieren, falls Sie nur eine leere Datei erzeugen, diese aber nicht bearbeiten möchten. Beachten Sie aber, dass die neuen Dateien nun als geöffnet gelten! Damit die Dateien vor dem Ende des Programms (z.B. an einer
10.3 Laufwerke, Verzeichnisse, Dateien
399
anderen Stelle im Code) auch bearbeitet werden können, müssen sie vorher geschlossen werden. Dazu hängen Sie an Create, CreateText oder Open die Methode Close an.
VERWEIS
VERWEIS
' Dateien nur erzeugen IO.File.Create("c:\test1\test1.bin").Close() IO.File.Open("c:\test1\test2.bin", IO.FileMode.OpenOrCreate).Close() IO.File.CreateText("c:\test1\test3.txt").Close()
Detailliertere Informationen zum Lesen und Schreiben von Textdateien (Klassen StreamReader und StreamWriter) sowie zum Umgang mit allgemeinen Dateien (Binärdateien, FileStream und verwandte Klassen) liefern die Abschnitte 10.5 und 10.6.
Per Default öffnet Create und Open die Dateien so, dass bis zum Schließen kein anderer Benutzer (kein anderes Programm) darauf zugreifen kann, nicht einmal lesend. CreateText ist etwas liberaler und lässt einen parallelen Lesezugriff zu, nicht aber einen Schreibzugriff. Wenn Sie explizit angeben möchten, ob andere Programme auf die geöffnete oder neu erstellte Datei zugreifen dürfen, müssen Sie zum Öffnen/Erstellen die Methode Open mit allen vier Parametern verwenden: IO.File.Open("name", modus, access, share). Genaue Informationen zu den Open-Parametern finden Sie in Abschnitt 10.6.1 im Zusammenhang mit dem FileStream-Objekt.
Dateien und Verzeichnisse kopieren, verschieben und löschen Mit Copy (File-Objekt) bzw. CopyTo (FileInfo-Objekt) können Sie eine Datei kopieren. Move bzw. MoveTo verschiebt eine Datei an einen anderen Ort bzw. gibt der Datei einen neuen Namen. Die beiden Methoden stehen auch für Verzeichnisse zur Verfügung. (Dagegen gibt es keine Möglichkeit, ganze Verzeichnisse zu kopieren.) IO.File.Copy("c:\test1\test3.txt", "c:\test1\test4.txt") IO.File.Move("c:\test1\test4.txt", "c:\test1\test5.txt") Delete löscht schließlich die angegebene Datei bzw. das leere Verzeichnis. Wenn Sie ein Verzeichnis inklusive des gesamten Inhalts löschen möchten, müssen Sie im optionalen zweiten Parameter True übergeben (siehe zweites Beispiel). IO.File.Delete("c:\test1\test5.txt") IO.Directory.Delete("c:\test1", True) Delete akzeptiert keine Muster. Wenn Sie beispielsweise alle *.bmp-Dateien in einem Verzeichnis löschen möchten, müssen Sie diese mit GetFiles("*.bmp") ermitteln und dann einzeln löschen. Die andere Möglichkeit besteht darin, das VB-Kommando Kill (Namensraum Microsoft.VisualBasic.FileSystem) einzusetzen, das auch Muster akzeptiert.
400
10 Dateien und Verzeichnisse
Dateien und Verzeichnisse in den Papierkorb verschieben Wenn Sie Dateien oder Verzeichnisse mit Delete löschen, dann ist diese Löschoperation endgültig. Oft wäre es praktischer, eine Datei oder ein Verzeichnis in den Papierkorb zu verschieben – aber die .NET-Klassenbibliothek bietet hierfür leider noch keine Möglichkeiten. Das Betriebssystem bietet diese Möglichkeit aber sehr wohl; aber um diese Funktion auch in VB.NET nutzen zu können, müssen Sie auf die in VB.NET eigentlich verpönten API-Aufrufe zurückgreifen. Die hier vorgestellte Funktion SafeDelete erwartet eine oder mehrere Zeichenketten oder ein String-Feld. Die Parameter geben an, welche Dateien oder Verzeichnisse in den Papierkorb befördert werden sollen. Vor dem Löschen erscheint automatisch eine Sicherheitsabfrage, ob die Dateien wirklich gelöscht werden sollen (siehe Abbildung 10.3).
Abbildung 10.3: Sicherheitsabfrage beim Löschen von Dateien
' Beispielprogramm dateien\safedelete ' Dateien und Verzeichnisse in den Papierkorb befördern Option Strict On ' SafeDelete testen Sub Main() ' ein Verzeichnis und zwei Dateien erzeugen IO.Directory.CreateDirectory("c:\test1") IO.File.Create("c:\test1\test1.bin").Close() IO.File.Create("c:\test1\test2.bin").Close() ' alles in den Papierkorb befördern SafeDelete.SafeDelete("c:\test1") End Sub SafeDelete greift auf die Betriebssystemfunktion SHFileOperation zurück. (Eine genaue Beschreibung der Declare-Syntax finden Sie in Abschnitt 12.7.) An diese Funktion wird in der Datenstruktur SHFILEOPSTRUCT eine Zeichenkette mit allen Dateinamen übergeben
(getrennt jeweils durch ein 0-Byte, abgeschlossen durch zwei 0-Bytes).
10.3 Laufwerke, Verzeichnisse, Dateien
401
Als gewünschte Operation wird Delete mit der Undo-Option angegeben (Strukturelemente wFunc und pFlags). Die Datenstruktur ist mit dem Attribut StructLayout aus dem Namensraum Runtime.InteropServices deklariert. Die Parameter des Attributs bewirken, dass die Elemente der Struktur in der angegebenen Reihenfolge angeordnet werden. (Diese Reihenfolge darf also nicht durch den VB.NET-Compiler verändert werden.) SafeDelete ist mit den dazugehörenden Deklarationen in eine Klasse gleichen Namens gekapselt. Nach außen hin ist nur die Funktion SafeDelete zugänglich. Die Funktion liefert als Ergebnis 0, wenn alles funktioniert hat, andernfalls einen Fehlercode ungleich 0. Class SafeDelete ' Konstanten für SHFileOperation Protected Const FO_DELETE As Integer = &H3 Protected Const FOF_ALLOWUNDO As Integer = &H40 ' Struktur für SHFileOperation _ Protected Structure SHFILEOPSTRUCT Dim hWnd As Integer Dim wFunc As Integer Dim pFrom As String Dim pTo As String Dim fFlags As Short Dim fAborted As Boolean Dim hNameMaps As Integer Dim sProgress As String End Structure ' Deklaration für die API-Funktion SHFileOperation Protected Declare Unicode Function SHFileOperation _ Lib "shell32.dll" (ByRef lpFileOp As SHFILEOPSTRUCT) As Integer ' nach außen hin zugängliche Funktion SafeDelete Shared Function SafeDelete(ByVal ParamArray files() As String) _ As Integer Dim fileOp As SHFILEOPSTRUCT fileOp.hWnd = 0 fileOp.wFunc = FO_DELETE fileOp.pFrom = String.Join(vbNullChar, files) + _ vbNullChar + vbNullChar fileOp.fFlags = FOF_ALLOWUNDO Return SHFileOperation(fileOp) End Function End Class
402
10 Dateien und Verzeichnisse
Zugriffsrechte von Dateien und Verzeichnissen ändern Sofern ein VB.NET-Programm auf einem Betriebssystem mit NTFS-Dateisystem ausgeführt wird, können die Zugriffsrechte von Dateien und Verzeichnissen sehr exakt eingestellt werden. Die erforderlichen Klassen (insbesondere FileIOPermission) und Methoden befinden sich in den Namensräumen System.Security und System.Security.Permissions, die in diesem Buch aber nicht beschrieben werden.
10.3.4 Spezielle Verzeichnisse, Dateien und Laufwerke ermitteln Dieser Abschnitt beschreibt verschiedene Wege, um besondere Verzeichnisse (z.B. das aktuelle Verzeichnis, das Windows-Verzeichnis, das temporäre Verzeichnis etc.) zu ermitteln. Dazu müssen Sie Methoden aus den verschiedensten .NET-Klassen (IO.DirectoryInfo, IO.Path, Environment, Reflection.Assembly etc.) einsetzen.
Aktuelles Verzeichnis ermitteln Für jedes Programm gilt ein Verzeichnis als das aktuelle Verzeichnis (per Default das Verzeichnis, von dem aus das Programm gestartet wurde). Sie können dieses Verzeichnis sehr einfach mit der VB-Methode CurDir ermitteln. Diese Methode liefert eine Zeichenkette mit dem aktuellen Verzeichnis zurück. Wenn Sie lieber .NET-Methoden einsetzen möchten, ermitteln Sie das Verzeichnis mit der Eigenschaft Environment.CurrentDirectory: Dim s As String = Environment.CurrentDirectory
Einen dritten Weg bietet die Klasse System.IO.DirectoryInfo: Dim dir As IO.DirectoryInfo = New IO.DirectoryInfo(".") s = dir.FullName
Falls Sie das Objekt dir anschließend nicht mehr benötigen, bietet sich die folgende Kurzform an (deren Nachteil allerdings darin besteht, dass der Code nicht besonders gut lesbar ist): Dim s As String = New IO.DirectoryInfo(".").FullName
Aktuelles Verzeichnis ändern Um das aktuelle Verzeichnis zu ändern, verwenden Sie entweder die Methode IO.Directory.SetCurrentDirectory oder das VB-Kommando ChDir. Beide Kommandos verändern gegebenenfalls auch das aktuelle Laufwerk. (In VB6 war dies bei ChDir nicht der Fall. Dort musste zusätzlich auch ChDrive ausgeführt werden.) IO.Directory.SetCurrentDirectory("c:\meinverzeichnis")
10.3 Laufwerke, Verzeichnisse, Dateien
403
Sowohl SetCurrentDirectory als auch ChDir funktionieren auch für Netzwerkverzeichnisse: IO.Directory.SetCurrentDirectory("\\mars\data")
Verzeichnis des laufenden Programms ermitteln: Mit dem nicht gerade besonders kurzen Ausdruck Reflection.Assembly.GetExecutingAssembly().Location können Sie den vollständigen Namen des gerade ausgeführten Programms ermitteln (z.B. "D:/code/vb.net/test/test2/ bin/test2.exe"). Um daraus das Verzeichnis zu ermitteln, können Sie die Methode IO.Path.GetDirectoryName zu Hilfe nehmen. Dim exename, exedir As String exename = Reflection.Assembly.GetExecutingAssembly().Location exedir = IO.Path.GetDirectoryName(exename)
Im Regelfall können Sie sich diese Arbeit ersparen, weil das Programmverzeichnis per Default mit dem aktuellen Verzeichnis übereinstimmt. Es kann aber sein, dass Sie das aktuelle Verzeichnis verändert haben, oder dass das Programm von einem anderen Verzeichnis aus gestartet wurde. In diesen Fällen weichen das aktuelle Verzeichnis und das Programmverzeichnis voneinander ab.
Temporäres Verzeichnis ermitteln Es gibt mehrere Möglichkeiten, das Verzeichnis für temporäre Dateien zu ermitteln. Die folgenden Programmzeilen zeigen einige Varianten. Die zweite und dritte Variante wertet jeweils die Umgebungsvariable temp aus (siehe auch Abschnitt 12.2.1).
HINWEIS
Dim s = s = s =
s As String IO.Path.GetTempPath() Environment.GetEnvironmentVariable("temp") Environ("temp")
Beachten Sie, dass das temporäre Verzeichnis – anscheinend aus Kompatibilitätsgründen – manchmal noch immer in der mit Windows 95 eingeführten 8+3-Kurzschreibweise angegeben ist. Beispielsweise liefern alle drei obigen Kommandos auf meinem Rechner s = "C:\DOKUME~1\ADMINI~1\LOKALE~1\Temp". Zum Glück verhalten sich die System.IO-Methoden demgegenüber tolerant, d.h., Verzeichnisse in dieser Schreibweise werden akzeptiert. Es scheint aber keinen einfachen Weg zu geben, der von dieser Schreibweise zu einem Verzeichnisnamen ohne Abkürzungen führt.
Namen für eine temporäre Datei ermitteln Wenn Sie eine temporäre Datei anlegen möchten, können Sie einen geeigneten Namen sehr einfach mit GetTempFileName auf die folgende Weise ermitteln: s = IO.Path.GetTempFileName()
404
10 Dateien und Verzeichnisse
Beachten Sie, dass GetTempFileName die temporäre Datei auch gleich erzeugt (mit einer Länge von null Bytes)!
Windows-Verzeichnis ermitteln Auch zur Ermittlung des Windows-Verzeichnisses bestehen mehrere Möglichkeiten. Bei der ersten und zweiten Variante wird jeweils die Umgebungsvariable windir ausgewertet. Bei der dritten Variante wird zuerst das Windows-Systemverzeichnis und dann dessen übergeordnetes Verzeichnis ermittelt. Diese Variante setzt also voraus, dass das Systemverzeichnis immer ein Unterverzeichnis des Windows-Verzeichnisses ist (was bei allen bisherigen Windows-Versionen der Fall war). Eine .NET-Methode, die das WindowsVerzeichnis direkt ermittelt, scheint es nicht zu geben. s = Environment.GetEnvironmentVariable("windir") s = Environ("windir") s = New IO.DirectoryInfo(Environment.SystemDirectory).Parent.FullName
Windows-Systemverzeichnis ermitteln In diesem Fall führt die Eigenschaft Environment.SystemDirectory direkt zum Ziel: s = Environment.SystemDirectory
Andere spezielle Verzeichnisse Die Methode Environment.GetFolderPath hilft dabei, diverse andere spezielle Verzeichnisse zu ermitteln. Die folgenden Zeilen geben zwei Beispiele: zuerst wird das Installationsverzeichnis für Programme ermittelt (z.B. "C:\Programme"), dann das Verzeichnis mit den persönlichen Dateien des Benutzers (z.B. "C:\Dokumente und Einstellungen\Administrator\ Eigene Dateien"). s = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) s = Environment.GetFolderPath(Environment.SpecialFolder.Personal)
Eine Referenz aller derartigen Verzeichnisse finden Sie in der Syntaxzusammenfassung. Wenn Sie sich rasch einen Überblick verschaffen möchten, können Sie auch die folgenden Zeilen ausführen. (Wenn Sie Probleme haben, den Code zu verstehen, werfen Sie bitte einen Blick in Abschnitt 4.4. Dort wird der Umgang mit Enum-Aufzählungen ausführlich erläutert.) Dim s As String Dim fld As Environment.SpecialFolder For Each s In System.Enum.GetNames(fld.GetType) fld = CType(System.Enum.Parse(fld.GetType, s), _ Environment.SpecialFolder) Console.WriteLine("{0}: {1}", s, Environment.GetFolderPath(fld)) Next
10.3 Laufwerke, Verzeichnisse, Dateien
405
Am Rechner verfügbare Laufwerke ermitteln Die am Rechner verfügbaren Laufwerke können Sie mit Environment.GetLogicalDrives oder mit IO.DirectoryGetLogicalDrives ermitteln. Beide Methoden liefern ein String-Feld mit den Namen der Laufwerke (also "A:\", "C:\", "D:\" etc.).
VERWEIS
Dim s As String Dim drvs As String() = Environment.GetLogicalDrives For Each s In drvs Console.WriteLine(s) Next
Wenn Sie mehr Informationen benötigen – z.B. welches der Laufwerke Festplatten, CD-ROMs etc. sind –, wird es leider ziemlich kompliziert. Sie müssen dann auf die Klassen der System.Management-Bibliothek zurückgreifen, die in Abschnitt 12.2.2 ganz kurz vorgestellt wird.
10.3.5 Bearbeitung von Datei- und Verzeichnisnamen Die Klasse System.IO.Path enthält eine Reihe von Methoden, mit denen Sie Zeichenketten bearbeiten können, die Datei- oder Verzeichnisnamen enthalten. GetFileName, GetFileNameWithoutExtension, GetPathRoot, GetDirectoryName und GetExtension helfen dabei, aus einem vollständigen Dateinamen dessen Kurzform (ohne Verzeichnisse), das Laufwerk, das Verzeichnis und die Dateikennung zu extrahieren. Dim full, file1, file2, folder, drive, extension As String ' Name des Programms, z.B. "D:/vb.net/test/test2/bin/test2.exe" full = Reflection.Assembly.GetExecutingAssembly.Location file1 = IO.Path.GetFileName(full) ' "test2.exe" file2 = IO.Path.GetFileNameWithoutExtension(full) ' "test2" drive = IO.Path.GetPathRoot(full) ' "D:\" folder = IO.Path.GetDirectoryName(full) ' "D:/vb.net/test/test2/bin" extension = IO.Path.GetExtension(full) ' ".exe" GetFullPath liefert den vollständigen Dateinamen. Die einzig sinnvolle Anwendung dieser
Methode ergibt sich dann, wenn Sie als Parameter einen Dateinamen ohne Verzeichnisse übergeben. In diesem Fall verbindet GetFullPath den Dateinamen mit dem aktuellen Verzeichnis. ChangeExtension ändert die Dateikennung. Das ist beispielsweise praktisch, wenn Sie eine
Sicherungskopie einer Datei erstellen möchten. Falls der ursprüngliche Dateiname noch keine Kennung enthält, wird diese hinzugefügt. Dim backup As String backup = IO.Path.ChangeExtension(full, ".bak") ' backup = "D:/vb.net/test/test2/bin/test2.bak"
406
10 Dateien und Verzeichnisse
Die Eigenschaften PathSeparator, VolumeSeparatorChar, InvalidPathChars etc. geben an, welche Zeichen in Dateinamen (nicht) verwendet werden können. Eine komplette Referenz dieser Eigenschaften gibt die Syntaxzusammenfassung. Die Eigenschaften sind vor allem dann interessant, falls die .NET-Umgebung in Zukunft auch unter anderen Betriebssystemen zur Verfügung stehen sollte. Beispielsweise werden unter Unix Verzeichnisse mit / statt mit \ getrennt. Durch die konsequente Anwendung der IO.Path-Eigenschaften und Methoden können Sie mögliche Portabilitätsprobleme vermeiden.
10.3.6 Beispiel – Backup automatisieren Meine Backup-Strategie beim Schreiben dieses Buchs bestand im Wesentlichen darin, dass ich einmal täglich die Textdatei auf eine zweite Festplatte kopiert habe (in der Hoffnung, dass nicht beide Festplatte gleichzeitig kaputt gehen würden). Um zusätzlich Sicherheit zu gewinnen, habe ich beim Kopieren nicht jedes Mal denselben Dateinamen verwendet, sondern einen Namen, der auch das Datum umfasst (also z.B. vbnet-2002-01-31.doc). Der Vorteil dieser Vorgehensweise besteht darin, dass ich so beispielsweise auch ein ganzes Kapitel rekonstruieren kann, wenn ich dieses irrtümlich gelöscht hätte (ganz einfach, indem ich das Kapitel aus einer älteren Backup-Version entnehme). Natürlich habe ich dieses Backup nicht manuell durchgeführt, sondern mit einem kleinen VB.NET-Programm, das ich jeden Tag vor dem Ausschalten des Rechners ausgeführt habe. Der Code beweist, dass auch mit wenigen Zeilen eine sinnvolle Aufgabe erfüllt werden kann. ' Beispiel dateien\backup Sub Main() Backup("d:\text\vbnet\vbnet.doc", "n:\bak\vbnet") End Sub Sub Backup(ByVal oldfilename As String, ByVal backupdir As String) ' filename: die zu sichernde Datei ' backupdir: das Verzeichnis mit den Backup-Dateien Dim newfilename, newfullname As String ' der neue Dateiname ohne Pfad newfilename = _ IO.Path.GetFileNameWithoutExtension(oldfilename) + _ String.Format("{0:-yyyy-MM-dd}", Today) + _ IO.Path.GetExtension(oldfilename) ' der neue Dateiname mit Pfad newfullname = IO.Path.Combine(backupdir, newfilename)
10.3 Laufwerke, Verzeichnisse, Dateien
407
' kopieren, evt. schon vorhandene Backup-Datei überschreiben Try IO.File.Copy(oldfilename, newfullname, True) Catch e As Exception MsgBox("Beim Backup ist ein Fehler aufgetreten: " & e.Message) End Try End Sub
10.3.7 Beispiel – Verzeichnisse synchronisieren Manchmal besteht der Wunsch, zwei Verzeichnisse (Verzeichnisbäume) miteinander zu synchronisieren. Das klassische Beispiel dafür ist die wechselseitige Bearbeitung von Dateien auf zwei unterschiedlichen Rechnern (z.B. Bürorechner und Notebook). Eine Synchronisation kann aber auch bei der effizienten Durchführung eines Backups eines ganzen Verzeichnisbaums helfen. Die einfachste Form der Synchronisation bestünde darin, einfach den gesamten Verzeichnisbaum vom Ursprungsort zum Zielort zu kopieren. Wenn sich aber nur einige wenige Dateien in einem riesigen Verzeichnisbaum geändert haben, ist diese Vorgehensweise ineffizient. Der folgende Algorithmus kopiert daher nur alle neuen bzw. veränderten Dateien von einem Quell- in ein Zielverzeichnis. Wenn das Zielverzeichnis leer ist, werden alle Dateien und Verzeichnisse kopiert. Damit kann der Algorithmus also auch dazu verwendet werden, um einen ganzen Verzeichnisbaum einfach zu kopieren. Beachten Sie bitte die folgenden Einschränkungen des Programms: •
Die Synchronisiation erfolgt nur einseitig. Wenn sich manche Dateien im Quellverzeichnis, andere im Zielverzeichnis verändert haben, wäre eine Synchronisation in beide Richtungen erforderlich.
•
Zur Feststellung, ob sich eine Datei verändert hat, wird ausschließlich das Datum der letzten Änderung verwendet. Das bedeutet, dass das Programm nur dann zuverlässig funktioniert, wenn das Zeitsystem im Quell- und im Zielverzeichnis übereinstimmt. (Wenn sich Quell- und Zielverzeichnis auf unterschiedlichen Rechnern befinden, sollte das unbedingt kontrolliert werden.)
•
Dateien, die im Quellverzeichnis gelöscht wurden, werden im Zielverzeichnis aus Sicherheitsgründen nicht ebenfalls gelöscht. Das führt allerdings dazu, dass auf einem Rechner gelöschte Dateien nach zwei Synchronisationsvorgängen (einmal in die eine, dann in die andere Richtung) wieder auftauchen.
•
Wenn das Zielverzeichnis nicht existiert, wird es erzeugt. Das funktioniert aber nur, wenn zumindest das Unterverzeichnis des Zielverzeichnisses bereits existiert. (Wenn das Zielverzeichnis c:\abc\def\ghi lautet, muss also zumindest c:\abc\def bereits existieren.)
•
Das Programm ist in keiner Weise gegen mögliche Fehler abgesichert.
408
10 Dateien und Verzeichnisse
Programmcode Der Programmcode beginnt in Main mit dem Aufruf von Synchronize. An dieses Unterprogramm werden die Namen des Quell- und Zielverzeichnisses übergeben. Synchronize testet, ob das Quellverzeichnis existiert und ruft dann die Funktion SynchronizeDirectory auf. Dort werden zuerst alle Dateien des aktuellen Verzeichnisses mit dem Zielverzeichnis verglichen; fehlende oder veraltete Dateien werden kopiert. Anschließend wird SynchronizeDirectory rekursiv für alle Unterverzeichnisse aufgerufen. Die Funktion liefert die Anzahl der kopierten Dateien zurück, die summiert wird. ' Beispiel dateien\synchronisation Sub Main() Synchronize("c:\test1", "c:\test2") End Sub Sub Synchronize(ByVal source As String, ByVal dest As String) Dim n As Integer Dim sourceDir As IO.DirectoryInfo ' testen, ob Quellverzeichnis existiert; falls nicht: Abbruch If IO.Directory.Exists(source) = False Then MsgBox("Quellverzeichnis " + source + " existiert nicht.") Exit Sub Else sourceDir = New IO.DirectoryInfo(source) End If ' Dateien und Verzeichnisse rekursiv kopieren n = SynchronizeDirectory(sourceDir, dest) MsgBox(n.ToString + " Dateien wurden kopiert bzw. aktualisiert.") End Sub Function SynchronizeDirectory(ByVal sourceDir As IO.DirectoryInfo, _ ByVal destDirName As String) As Integer Dim subdir, destDir As IO.DirectoryInfo Dim sourceFile As IO.FileInfo Dim destFileName As String Dim changedFiles As Integer ' testen, ob Zielverzeichnis überhaupt existiert; ' gegebenenfalls erzeugen destDir = New IO.DirectoryInfo(destDirName) If destDir.Exists = False Then destDir.Create() ' alle Dateien des Quellverzeichnisses bearbeiten ' falls Datei im Zielverzeichnis nicht existiert, ' oder wenn sie älter ist: kopieren bzw. überschreiben
10.3 Laufwerke, Verzeichnisse, Dateien
409
For Each sourceFile In sourceDir.GetFiles() destFileName = IO.Path.Combine(destDir.FullName, _ sourceFile.Name) If IO.File.Exists(destFileName) = False OrElse _ sourceFile.LastWriteTime > _ IO.File.GetLastWriteTime(destFileName) Then sourceFile.CopyTo(destFileName, True) IO.File.SetLastWriteTime(destFileName, sourceFile.LastWriteTime) changedFiles += 1 End If Next ' rekursiv alle Unterverzeichnisse bearbeiten For Each subdir In sourceDir.GetDirectories() changedFiles += SynchronizeDirectory( _ subdir, IO.Path.Combine(destDirName, subdir.Name)) Next Return changedFiles End Function
10.3.8 Syntaxzusammenfassung Informationen über Dateien und Verzeichnisse ermitteln Die Klassen DirectoryInfo und FileInfo sind beide von der Klasse FileSystemInfo abgeleitet. Deswegen gibt es eine Menge gemeinsamer Eigenschaften und Methoden, die im ersten Syntaxkasten beschrieben werden. Spezifische Schlüsselwörter der beiden Klassen folgen in den beiden weiteren Syntaxkästen. System.IO.DirectoryInfo und .FileInfo-Klassen Attributes
gibt die Datei- bzw. Verzeichnisattribute an. Der Wert setzt sich aus der Kombination von IO.FileAttributesKonstanten zusammen (siehe nächste Überschrift).
CreationTime
gibt den Zeitpunkt an, zu dem das Verzeichnis bzw. die Datei erzeugt wurde (Date-Objekt).
Exists
gibt an, ob das angegebene Verzeichnis bzw. die Datei tatsächlich existiert.
Extension
gibt die Dateikennung an (z.B. ".bmp").
FullName
gibt den vollständigen Namen an (inklusive Laufwerk und allen Verzeichnissen).
LastAccessTime
gibt den Zeitpunkt des letzten Lesezugriffs an.
LastWriteTime
gibt den Zeitpunkt der letzten Veränderung an.
410
10 Dateien und Verzeichnisse
System.IO.DirectoryInfo und .FileInfo-Klassen Name
gibt den Datei- oder Verzeichnisnamen ohne Unterverzeichnisse an.
Spezifische Schlüsselwörter der System.IO.FileInfo-Klasse Directory
liefert ein DirectoryInfo-Objekt des Verzeichnisses, in dem sich die Datei befindet.
DirectoryName
liefert eine Zeichenkette mit dem Verzeichnis, in dem sich die Datei befindet.
Length
liefert die Größe der Datei (in Byte).
Spezifische Schlüsselwörter der System.IO.DirectoryInfo-Klasse GetDirectories()
liefert alle Unterverzeichnisse als DirectoryInfo-Feld.
GetDirectories("*.abc")
wie GetDirectories(), liefert aber nur Unterverzeichnisse, die der angegebenen Maske entsprechen.
GetFiles()
liefert alle Dateien als FileInfo-Feld.
GetFiles("*.abc")
wie GetFiles(), liefert aber nur die Dateien, die der angegebenen Maske entsprechen.
GetFileSystemInfos()
liefert alle Unterverzeichnisse und Dateien in Form eines FileSystemInfo-Felds.
GetFileSystemInfos("*.abc")
wie GetFileSystemInfos(), liefert aber nur Dateien und Verzeichnisse, die der Maske entsprechen.
Parent
liefert das übergeordnete Verzeichnis als DirectoryInfoObjekt.
Root
liefert das Wurzelverzeichnis (z.B. C:\ oder \\mars\data) als DirectoryInfo-Objekt.
Datei- und Verzeichnisattribute Die folgende Tabelle fasst die möglichen Verzeichnis- und Dateiattribute zusammen. Beachten Sie, dass manche Attribute nur dann zur Verfügung stehen, wenn ein NTFS-Dateisystem vorliegt (Windows NT, 2000, XP). System.IO.FileAttributes-Aufzählung Archive
die Datei wurde durch ein Backup-Programm archiviert und seither nicht mehr verändert.
Compressed
die Datei ist komprimiert.
10.3 Laufwerke, Verzeichnisse, Dateien
411
System.IO.FileAttributes-Aufzählung Device
es handelt sich nicht um eine normale Datei, sondern um ein so genanntes Device, das den direkte Zugriff auf die Hardware ermöglicht.
Directory
es handelt sich nicht um eine Datei sondern um ein Verzeichnis.
Encrypted
die Datei ist verschlüsselt.
Hidden
die Datei ist versteckt.
Normal
es handelt sich um eine gewöhnliche Datei.
NotContentIndexed
die Datei ist nicht Teil eines Index.
Offline
die Datei ist nur über ein Netzwerk/Internet zugänglich, momentan besteht aber keine Verbindung.
ReadOnly
die Datei darf nicht verändert werden.
ReparsePoint
die Datei enthält zusätzliche, benutzerspezifische Informationen.
SparseFile
die Datei besteht überwiegend aus 0-Bytes.
System
es handelt sich um eine Systemdatei.
Temporary
es handelt sich um eine temporäre Datei.
Dateien öffnen bzw. neu erzeugen System.IO.File-Methoden AppendText("filename")
liefert ein StreamWriter-Objekt, mit dessen Hilfe Sie an die bereits vorhandene Datei Text anfügen können.
Create("name")
erzeugt bzw. überschreibt die angegebene Datei und liefert ein FileStream-Objekt zur weiteren Bearbeitung.
CreateText("name")
wie oben, liefert aber ein ein StreamWriter-Objekt zum Schreiben der Datei.
Open("name", mode, [access [,share]])
öffnet die Datei name und liefert ein FileStream-Objekt zur weiteren Bearbeitung. mode gibt an, wie die Datei geöffnet wird (IO.FileModeAufzählung). access gibt den Zugriffsmodus an (IO.FileAccessAufzählung). share gibt an, ob ein Mehrfachzugriff zulässig ist (IO.FileSharing-Aufzählung).
OpenRead("name")
öffnet die vorhandene Datei und liefert ein FileStreamObjekt zum Lesen der Daten.
412
10 Dateien und Verzeichnisse
System.IO.File-Methoden OpenText("name")
öffnet die vorhandene Textdatei und liefert ein StreamReader-Objekt zum Lesen der Daten.
OpenWrite("name")
öffnet die vorhandene Datei und liefert ein FileStreamObjekt zum Lesen und Schreiben von Daten.
System.IO.FileInfo-Methoden liefert ein StreamWriter-Objekt, um am Ende der Datei Text hinzuzufügen.
Create()
erzeugt bzw. überschreibt die Datei und liefert ein FileStream-Objekt zum Schreiben der Datei.
CreateText()
wie oben, liefert aber ein StreamWriter-Objekt zum Schreiben der Textdatei.
Open(mode, [access [,share]])
siehe File.Open.
OpenRead(), OpenWrite()
siehe File.OpenRead() bzw. File.OpenWrite().
OpenText()
siehe File.OpenText().
VERWEIS
AppendText()
Eine Referenz der Aufzählungen IO.FileMode, IO.FileAccess und IO.FileSharing finden Sie in der Syntaxreferenz zum FileStream-Objekt in Abschnitt 10.6.4.
Verzeichnisse neu erzeugen System.IO.Directory-Methoden CreateDirectory("name")
erzeugt das Verzeichnis und liefert ein DirectoryInfo-Objekt.
System.IO.DirectoryInfo-Methoden Create()
erzeugt das Verzeichnis. Wenn das Verzeichnis bereits existiert, bleibt es unverändert.
CreateSubdirectory("name")
erzeugt ein Unterverzeichnis zum aktuellen Verzeichnis und liefert ein neues DirectoryInfo-Objekt zur weiteren Bearbeitung.
10.3 Laufwerke, Verzeichnisse, Dateien
413
Vorhandene Dateien und Verzeichnisse kopieren, verschieben, löschen System.IO.File-Methoden Copy("name1", "name2")
kopiert die Datei name1. Falls name2 schon existiert, tritt ein Fehler auf.
Copy("name1", "name2", True)
kopiert die Datei name1. Falls name2 schon existiert, wird diese Datei überschrieben.
Delete("name")
löscht die Datei.
Exists("name")
testet, ob die Datei existiert.
Move("name1", "name2")
benennt name1 in name2 um. Quelle und Ziel dürfen sich auch auf unterschiedlichen Verzeichnissen bzw. Laufwerken befinden, dann wird die Datei verschoben.
System.IO.FileInfo-Methoden CopyTo("zielname")
kopiert die Datei zu zielname. Falls zielname schon existiert, tritt ein Fehler auf. Die Methode liefert ein neues FileInfo-Objekt, das auf die neue Datei verweist.
CopyTo("zielname", True)
wie oben, aber zielname wird gegebenenfalls überschrieben.
Delete()
löscht die Datei.
MoveTo("zielname")
benennt die Datei um bzw. verschiebt sie in ein anderes Verzeichnis oder Laufwerk. Das FileInfo-Objekt verweist anschließend auf zielname.
System.IO.Directory-Methoden Delete("name")
löscht das Verzeichnis, wenn es leer ist.
Delete("name", True)
löscht das Verzeichnis mit seinem gesamten Inhalt (auch Unterverzeichnisse).
Exists("name")
testet, ob das Verzeichnis existiert.
Move("name1", "name2")
verschiebt das Verzeichnis bzw. benennt es um.
System.IO.DirectoryInfo-Methoden Delete()
löscht das Verzeichnis, wenn es leer ist.
Delete(True)
löscht das Verzeichnis samt Inhalt.
MoveTo("zielname")
benennt das Verzeichnis um bzw. verschiebt es in ein anderes Verzeichnis oder Laufwerk. Das DirectoryInfoObjekt verweist anschließend auf zielname.
414
10 Dateien und Verzeichnisse
Spezielle Verzeichnisse ermitteln Microsoft.VisualBasic.FileSystem-Klasse CurDir()
liefert das aktuelle Verzeichnis als Zeichenkette.
ChDir("c:\test")
ändert das aktuelle Verzeichnis und Laufwerk.
ChDrive("d:")
ändert das aktuelle Laufwerk. Damit wird das für dieses Laufwerk zuletzt aktuelle Verzeichnis wieder zum aktuellen Verzeichnis.
Environ("temp")
liefert das temporäre Verzeichnis.
Environ("windir")
liefert das Windows-Verzeichnis.
System.IO-Namensraum DirectoryInfo(".").FullName
ermittelt das aktuelle Verzeichnis.
Directory. _ SetCurrentDirectory("c:\test")
ändert das aktuelle Verzeichnis und Laufwerk.
System.IO.Path-Klasse GetTempPath()
liefert den Namen des temporären Verzeichnisses.
GetTempFileName()
liefert den Namen einer temporären Datei, die zu diesem Zweck als leere Datei erzeugt wird.
System.Reflection-Namensraum Assembly. _ GetExecutingAssembly. _ Location
liefert den vollständigen Namen (inklusive Laufwerk und Verzeichnissen) des laufenden VB.NET-Programms.
System.Environment-Klasse CurrentDir
liefert das aktuelle Verzeichnis.
GetEnvironmentVariable(var)
liefert das temporäre Verzeichnis (var="temp") bzw. das Windows-Verzeichnis (var="windir").
GetFolderPath(fld)
liefert ein spezielles Verzeichnis. fld ist eine Konstante der System.Environment.SpecialFolder-Aufzählung (siehe nächste Box).
GetLogicalDrives
liefert ein String-Feld mit den Namen aller Laufwerke des Rechners (z.B. "C:\").
10.3 Laufwerke, Verzeichnisse, Dateien
415
System.Environment.SpecialFolder-Aufzählung für GetFolderPath ApplicationData
persönliche Anwendungsdaten
CommonApplicationData
allgemeine Anwendungsdaten
CommonProgramFiles
gemeinsame Dateien (C:\Programme\Gemeinsame Dateien)
Cookies
Cookies-Verzeichnis
DesktopDirectory
Desktop-Verzeichnis
Favorites
Favoriten des Internet Explorers
History
Verlauf des Internet Explorers
InternetCache
Cache des Internet Explorers
LocalApplicationData
lokale Anwendungsdaten
Personal
persönliche Dateien (C:\Dokumente und Einstellungen\user\Eigene Dateien)
ProgramFiles
Installationsverzeichnis für Programme
Programs
Verzeichnis des Startmenüs PROGRAMME
Recent
zuletzt genutzte Dokumente
SendTo
Verzeichnis der SENDEN-AN-Programme
StartMenu
Verzeichnis des Startmenüs
Startup
Autostart-Verzeichnis
System
Windows-Systemverzeichnis
Templates
Vorlagenverzeichnis
Bearbeitung von Datei- und Verzeichnisnamen (IO.Path) System.IO.Path-Klasse – Datei- und Verzeichnisnamen bearbeiten ChangeExtension(s, ext)
ändert die Kennung des Dateinamens.
GetDirectoryName(s)
liefert das Laufwerk und das Verzeichnis.
GetExtension(s)
liefert die Dateikennung (z.B. ".bmp").
GetFileName(s)
liefert den Dateinamen ohne Verzeichnisse.
GetFileNameWithoutExtension(s)
wie GetFileName, es wird aber auch die Dateikennung entfernt.
GetFullPath(s)
verbindet einen ohne Verzeichnis angegebenen Dateinamen mit dem aktuellen Verzeichnis.
GetPathRoot(s)
liefert das Laufwerk (z.B. "C:\" oder "\\mars\data").
HasExtension(s)
testet, ob der Dateiname eine Kennung hat.
416
10 Dateien und Verzeichnisse
System.IO.Path-Klasse – Sonderzeichen DirectorySeparatorChar
liefert das Zeichen zur Trennung von Verzeichnissen: Windows: "\" Apple: ":" Unix: "/"
AltDirectorySeparatorChar
gibt eine (nur für den System.IO-Namensraum!) zulässige Alternative zu DirectorySeparatorChar an: Windows: "/" Apple: "/" Unix: "\"
InvalidPathChars
liefert ein Char-Feld mit Zeichen, die nicht in Dateinamen vorkommen dürfen. Unter Windows sind das die Zeichen "", "|", das Hochkomma " sowie 0-Codes. (Die Online-Dokumentation nennt weitere Zeichen, die aber in InvalidPathChars nicht enthalten sind.)
PathSeparator
liefert das Zeichen zur Trennung mehrerer Namen voneinander. Unter Windows ist das das Zeichen ";".
VolumeSeparatorChar
liefert das Zeichen zur Angabe von Laufwerken. Windows: ":" Apple: ":" Unix: "/"
10.4
Standarddialoge
VERWEIS
Immer wieder wird es in Windows-Programmen vorkommen, dass der Anwender in einem Dialog einen Dateinamen auswählen soll, beispielsweise um eine Datei zu laden oder um eigene Daten in einer (eventuell neuen) Datei zu speichern. Die Bibliothek System.Windows.Forms.dll enthält zu diesem Zweck fertige Dialoge, die Thema dieses Abschnitts sind. OpenFileDialog und SaveFileDialog gehören zu einer ganzen Gruppe so genannter Standarddialoge, die alle im Namensraum System.Windows.Forms definiert sind. All-
gemeine Informationen zu diesen Standarddialogen finden Sie in Abschnitt 15.5.
10.4.1 Dateiauswahl Um diese Dialoge in einem eigenen Windows-Programm anzuwenden, fügen Sie OpenFileDialog oder SaveFileDialog aus der Toolbox in Ihr Formular ein. Die resultierenden Objekte OpenFileDialog1 bzw. SaveFileDialog1 (die Sie natürlich umbenennen können) wenden Sie dann entsprechend dem folgenden Muster an:
10.4 Standarddialoge
417
' Beispiel dateien\open-save-dialog Private Sub Button1_Click(...) Handles Button1.Click If OpenFileDialog1.ShowDialog() = DialogResult.OK Then ' Datei laden; der Dateiname befindet sich in ' OpenFileDialog1.FileName End If End Sub Private Sub Button2_Click(...) Handles Button2.Click If SaveFileDialog1.ShowDialog() = DialogResult.OK Then ' Datei speichern; der Dateiname befindet sich in ' SaveFileDialog1.FileName End If End Sub
Abbildung 10.4: Standarddialog zur Dateiauswahl
Open- und SaveFileDialog sind von der Basisklasse FileDialog abgeleitet. Bevor Sie den Dialog mit ShowDialog aufrufen, können Sie eine Menge Eigenschaften einstellen, um den Aus-
wahlprozess zu steuern (siehe Syntaxzusammenfassung). Der Unterschied zwischen den beiden Dialogen besteht darin, dass der Anwender bei OpenFileDialog nur existierende Dateien auswählen darf, während bei SaveFileDialog auch der Name einer noch nicht existierenden Datei angegeben werden darf. Wenn mit SaveFileDialog eine vorhandene Datei
418
10 Dateien und Verzeichnisse
ausgewählt wird, erscheint eine Sicherheitsabfrage, ob diese Datei überschrieben werden soll. Beachten Sie, dass die Methode ShowDialog den Dialog nur anzeigt, die Datei aber anschließend weder lädt noch speichert. Dafür sind Sie selbst verantwortlich. Den Dateinamen können Sie der Eigenschaft FileName entnehmen.
Mehrfachauswahl Beim OpenFileDialog können Sie auch mehrere Dateien gleichzeitig auswählen. Dazu setzen Sie vor dem Anzeigen des Dialogs die Eigenschaft Multiselect auf True. Nach der Auswahl können Sie mit FileNames auf ein String-Feld zugreifen, das die einzelnen Dateinamen enthält.
Verwendung in Konsolenanwendungen Die Dialoge können auch in Konsolenanwendungen eingesetzt werden. Dazu müssen Sie einen Verweis auf die Bibliothek System.Windows.Forms.dll einrichten (da diese Bibliothek per Default bei Konsolenanwendungen nicht zur Verfügung steht). Außerdem müssen Sie ein Objekt der Klassen Open- oder SaveFileDialog erstellen: ' Beispiel dateien\open-file-console Sub Main() Dim ofd As New Windows.Forms.OpenFileDialog() Console.WriteLine("Wählen Sie einen Dateinamen aus!") If ofd.ShowDialog() = Windows.Forms.DialogResult.OK Then Console.WriteLine("Dateiname: {0}", ofd.FileName) Else Console.WriteLine("Dateiauswahl wurde abgebrochen.") End If Console.WriteLine("Return drücken") Console.ReadLine() End Sub
Syntaxzusammenfassung Windows.Forms.OpenFileDialog und .SaveFileDialog – Methoden und Eigenschaften ShowDialog()
zeigt den Dialog an und liefert DialogResult.OK, wenn die Dateiauswahl ordnungsgemäß beendet wird.
AddExtension
gibt an, ob an den ausgewählten Dateinamen automatisch eine Typerweiterung angefügt werden soll, wenn der Benutzer keine angibt. Die Erweiterung wird aus Filter entnommen.
10.4 Standarddialoge
419
Windows.Forms.OpenFileDialog und .SaveFileDialog – Methoden und Eigenschaften CheckFileExists
gibt an, ob nur bereits existierende Dateinamen ausgewählt werden dürfen (per Default False).
CheckPathExists
gibt an, ob Dateien nur aus schon existierenden Verzeichnissen ausgewählt werden dürfen (per Default True).
FileName
enthält den vollständigen Dateinamen.
FileNames
enthält ein String-Feld mit allen ausgewählten Dateinamen (nur bei einer Mehrfachauswahl).
Filter
enthält einen oder mehrere Filter für die Dateitypen. Die Zeichenkette muss nach dem folgenden Muster zusammengesetzt werden: "text1|filter1|text2|filter2", also beispielsweise "Textdateien|*.txt".
InitialDirectory
gibt das Startverzeichnis für die Auswahl an.
Multiselect
gibt an, ob mehrere Dateien zugleich ausgewählt werden dürfen.
Title
gibt an, welcher Text im Fenstertitel des Dialogs angezeigt werden soll.
ValidateNames
gibt an, ob nur gültige Windows-Dateinamen akzeptiert werden sollen (per Default True).
10.4.2 Verzeichnisauswahl Die offiziellen Standarddialoge eignen sich zwar zur Auswahl einer bestehenden oder neuen Datei, nicht aber zur Auswahl eines Verzeichnisses. In manchen Anwendungen ist aber die Auswahl eines Verzeichnisses sehr wohl erforderlich (beispielsweise wenn ein Verzeichnis angegeben werden soll, aus dem Dateien gelesen bzw. in dem Dateien gespeichert werden sollen). Versteckt in den Tiefen der .NET-Bibliothek erfüllt die Klasse Windows.Forms.Design.FolderNameEditor.FolderBrowser aus der Bibliothek System.Design.dll genau diese Aufgabe: ShowDialog zeigt einen Dialog an, in dem Sie ein bestehendes Verzeichnis auswählen bzw. ein neues Verzeichnis erstellen können. Die Eigenschaft ReturnPath enthält anschließend den
VORSICHT
Verzeichnisnamen. Die .NET-Dokumentation zu FolderNameEditor.FolderBrowser enthält folgenden Hinweis: This type supports the .NET Framework infrastructure and is not intended to be used directly from your code. Mit anderen Worten: Auch wenn der hier vorgestellte Code funktioniert, ist nicht sicher, dass dies auch für künftige .NET-Versionen gilt.
420
10 Dateien und Verzeichnisse
Definition der neuen Klasse FolderBrowserDialog Bevor diese Klasse wie die anderen in diesem Abschnitt vorgestellten Standarddialoge verwendet werden kann, ist allerdings etwas Vorarbeit erforderlich. Im folgenden Beispielprogramm wird die neue Klasse FolderBrowserDialog definiert, die von FolderNameEditor.FolderBrowser abgeleitet ist. Das ist erforderlich, weil FolderNameEditor.FolderBrowser als Protected gilt und nicht unmittelbar verwendet werden kann. (Die Idee des Beispiels stammt übrigens von Frank Eller und wurde als Beitrag der News-Gruppe microsoft.public.dotnet.languages.csharp am 15.8.2001 publiziert.) Der Code für die neue Klasse FolderBrowserDialog enthält keine Besonderheiten. Die Klasse ist von FolderNameEditor abgeleitet. Intern wird ein FolderNameEditor.FolderBrowser-Objekt zur Anzeige des Dialogs verwendet. Nach außen hin sind nur die Eigenschaften Description und ReturnPath sowie die Methode ShowDialog zugänglich. ' ' ' '
Beispiel benutzeroberflaeche\folderbrowser Klassendatei folderbrowserdialog.vb dieser Code setzt voraus, dass ein Verweis auf die .NET-Bibliothek System.Design.dll eingerichtet wird
Public Class FolderBrowserDialog Inherits Windows.Forms.Design.FolderNameEditor Private fb As New _ Windows.Forms.Design.FolderNameEditor.FolderBrowser() Private fbReturnPath As String Public Description As String Public ReadOnly Property ReturnPath() As String Get Return fbReturnPath End Get End Property Public Function ShowDialog() As DialogResult Dim result As DialogResult fb.Description = Me.Description fb.StartLocation = FolderBrowserFolder.MyComputer result = fb.ShowDialog() If (result = DialogResult.OK) Then Me.fbReturnPath = fb.DirectoryPath Else Me.fbReturnPath = String.Empty End If Return result End Function End Class
10.5 Textdateien lesen und schreiben
421
Anwendung der Klasse FolderBrowserDialog Die Anwendung der Klasse FolderBrowserDialog erfolgt ähnlich wie bei den anderen Standarddialogen: Der einzige Unterschied besteht darin, dass die Entwicklungsumgebung FolderBrowserDialog nicht als Steuerelement erkennt und dass Sie ein Objekt dieser Klasse deswegen selbst erzeugen müssen. Den Dialog zeigen Sie mit ShowDialog an; anschließend können Sie der Eigenschaft ReturnPath den Verzeichnisnamen entnehmen. ' Beispiel benutzeroberflaeche\folderbrowser ' Formulardatei form1.vb Private Sub Button1_Click(...) Handles Button1.Click Dim fb As New FolderBrowserDialog() fb.Description = "Verzeichnis mit den Quelldateien auswählen" If fb.ShowDialog() = DialogResult.OK Then Label1.Text = fb.ReturnPath Else Label1.Text = "Auswahl wurde abgebrochen ..." End If End Sub
Abbildung 10.5: Standarddialog zur Verzeichnisauswahl
10.5
Textdateien lesen und schreiben
Mit den Methoden der Klassen StreamReader und StreamWriter können Sie Textdateien komfortabel lesen und schreiben. Der Dateizugriff ist rein sequentiell, es ist daher nicht möglich, die Schreib- oder Leseposition unmittelbar zu beeinflussen.
422
10 Dateien und Verzeichnisse
10.5.1 Codierung von Textdateien Wenn Sie mit Windows in Westeuropa arbeiten, erwarten Sie im Regelfall, dass die Zeichen in Textdateien mit dem ANSI-Zeichensatz (Codeseite 1252) codiert sind. Diese Dateien werden dann oftmals als ASCII-Dateien bezeichnet, was genau genommen aber falsch ist: ASCII definiert nur die Zeichen mit Codes von 0-127. ANSI lässt dagegen Codes bis 255 zu und ermöglicht so auch die Darstellung vieler Sonderzeichen in Europa verbreiteteter Sprachen (inklusive äöüß). Sobald Sie aber über die Grenzen Westeuropas hinaussehen, wird es komplizierter: So sind in Osteuropa andere Codeseiten üblich, die mit ANSI inkompatibel sind. Und im asiatischen Sprachraum reicht ein Byte pro Zeichen sowieso nicht aus. Der Ausweg aus diesem Dilemma heißt Unicode – also ein Zeichensatz, mit dem fast alle weltweit vorkommenden Zeichen codiert werden können (siehe auch Abschnitt 8.2.5). Allerdings gibt es mehrere Möglichkeiten, einzelne Unicode-Zeichen intern darzustellen (UTF-7, -8, -16), so dass die Bezeichnung Unicode-Datei noch nicht eindeutig ist. Per Default verwenden StreamReader und StreamWriter die Unicode-UTF-8-Codierung. StreamReader und -Writer kommen natürlich auch mit einigen weiteren Textcodierungen zurecht (z.B. ASCII ANSI, UTF-16), das muss dann aber explizit angegeben werden. (Beispiele folgen im Verlauf dieses Abschnitts.) Die UTF-8-Codierung bedeutet, dass ASCII-Zeichen mit einem Byte pro Zeichen, alle anderen Unicode-Zeichen aber mit zwei bis vier Byte codiert werden. Solange eine UTF-8Textdatei ausschließlich US-Zeichen enthält, ist sie nicht von einer ASCII-Datei zu unterscheiden.
TIPP
Auch wenn dieses Dateiformat wahrscheinlich eine große Zukunft hat (weil es auch unter Unix/Linux gebräuchlich ist), gibt es momemtan unter Windows nur relativ wenige Programme, die mit solchen Dateien umgehen können. Dazu zählen unter anderem die aktuellen Versionen von Microsoft Word sowie der Editor notepad.exe (z.B. jene Version, die mit Windows 2000 mitgeliefert wird). Word 2000 kann reine Textdateien in verschiedenen Unicode-Varianten speichern (DATEI|SPEICHERN UNTER, Dateityp CODIERTE TEXTDATEI). Bisweilen hat Word 2000 aber Probleme, derartige Dateien wieder zu öffnen. Diese Probleme treten vor allem bei ganz kurzen Dateien auf, wo es schwierig ist, die Codierung korrekt zu erkennen. Zeichen außerhalb des ASCII-Zeichensatzes werden dann falsch angezeigt. Bei längeren Dateien erkennt Word die Codierung meist selbstständig bzw. zeigt einen Konvertierungsdialog an, in dem Sie die gewünschte Codierung auswählen können.
Wie kann die Codierung einer Textdatei erkannt werden? Bei Textdateien mit einer Codierung mit einem Byte pro Zeichen gibt es in der Regel keine Kennzeichnung der Codierung. Damit also ein Austausch von Textdateien zwischen
10.5 Textdateien lesen und schreiben
423
verschiedenen Personen oder Programmen gelingt, muss der Empfänger wissen, welche Codierung der Sender verwendet hat! Etwas besser sieht es bei Unicode-Dateien aus: Dort ist für einige Codierungsvarianten eine Kennzeichnung durch die ersten Bytes vorgesehen. Genau genommen dient diese Kennzeichnung nur dazu, um die Bytereihenfolge anzugeben (byte order mark, kurz BOM). Diese kann bei UTF-16- und UTF-32-Dateien je nach Prozessorarchitektur variieren. Bei UTF-8-Dateien ist eine derartige Kennzeichnung dagegen nicht erforderlich, weil die Bytereihenfolge unabhängig von der Prozessorarchitektur ist. Dennoch sind auch für UTF-8Dateien drei Kennzeichnungsbytes vorgesehen, die aber optional sind. (In der Praxis werden Sie überwiegend auf UTF-8-Dateien ohne Kennzeichnung stoßen! Beachten Sie auch, dass es nicht üblich ist, einzelne Zeichenketten durch ein BOM zu kennzeichnen – etwa wenn Sie Unicode-Zeichenketten in einer Tabelle einer Datenbank speichern.) Die folgende Tabelle gibt die hexadezimalen Codes der ersten Bytes für die wichtigsten Unicode-Varianten an: FF FE FE FF 00 00 FE FF FF FE 00 00 EF BB BF
Unicode UTF-16 Unicode UTF-16 Big-Endian (umgekehrte Byte-Reihenfolge) Unicode UTF-32 Unicode UTF-32 Big-Endian (umgekehrte Byte-Reihenfolge) Unicode UTF-8 (optionale Kennzeichnung, fehlt oft!)
10.5.2 Textdateien lesen (StreamReader) Unicode-Textdatei öffnen Die folgenden Zeilen zeigen einige Möglichkeiten, um eine vorhandene Unicode-Textdatei zu öffnen. In jedem Fall ist das Ergebnis ein StreamReader-Objekt, mit dessen Hilfe die Datei dann gelesen werden kann. (Bei der letzten Variante bezieht sich New auf ein IO.FileInfoObjekt, auf das anschließend die Methode OpenText angewendet wird.) Dim sr As IO.StreamReader sr = New IO.StreamReader("c:\test\test1.txt") sr = New IO.StreamReader(io_stream_object) sr = IO.File.OpenText("c:\test\test2.txt") sr = New IO.FileInfo("c:\test\test3.txt").OpenText()
Bei allen drei Varianten werden die ersten Bytes der Datei ausgewertet, um die UnicodeVariante zu erkennen. Dateien, die als UTF-8- und UTF-16-Dateien gekennzeichnet sind (siehe Tabelle oben), werden korrekt behandelt. Alle anderen Dateien werden so verarbeitet, als wären sie gemäß UTF-8 codiert. Wenn die Datei in einer anderen Codierung vorliegt, führt dies natürlich zu fehlerhaften Ergebnissen. Das Beispielprogramm dateien\unicode\textdatei-anzeigen (Abbildung 10.6) demonstriert, was passiert, wenn die von StreamReader verwendete Defaultcodierung UTF-8 und die tatsächliche Codierung einer Textdatei nicht übereinstimmen. Nach dem Start des Programms können Sie eine Textdatei auswählen. Dabei haben Sie die Wahl zwischen meh-
424
10 Dateien und Verzeichnisse
reren Beispieldateien, die alle denselben Text enthalten – aber in unterschiedlichen Codierungen. Die Datei wird zur Gänze geladen und im Textfeld angezeigt.
Abbildung 10.6: StreamReader interpretiert die ANSI-Datei fälschlich als UTF-8-Datei und verschluckt deswegen alle deutschen Sonderzeichen
Die folgenden Zeilen zeigen die Ereignisprozedur zum Laden der Textdatei. Die Dateiauswahl erfolgt mit dem OpenFileDialog-Steuerelement. Anschließend wird ein StreamReaderObjekt erzeugt und mit der Methode ReadToEnd die gesamte Datei gelesen. Welche Codierung StreamReader intern verwendet, können Sie mit der Eigenschaft CurrentEncoding ermitteln. Diese Eigenschaft liefert als Ergebnis ein Objekt der Klasse Text.Encoding. Beachten Sie, dass CurrentEncoding erst dann ausgewertet werden darf, nachdem die ersten Zeichen der Datei gelesen wurden! Erst dann versucht StreamReader, die Codierung automatisch zu erkennen. Beachten Sie auch, dass die von StreamReader eingesetzte Codierung keineswegs immer korrekt ist (was Abbildung 10.6 ja beweist). ' Beispielprogramm dateien\unicode\textdatei-anzeigen Private Sub Button1_Click(...) Handles Button1.Click If OpenFileDialog1.ShowDialog() = DialogResult.OK Then ' Datei lesen und in Textbox darstellen Dim sr As New IO.StreamReader(OpenFileDialog1.FileName) TextBox1.Text = sr.ReadToEnd() ' Infos über Datei anzeigen LabelFileName.Text = OpenFileDialog1.FileName LabelCode.Text = sr.CurrentEncoding.EncodingName sr.Close() End If End Sub
10.5 Textdateien lesen und schreiben
425
Textdatei in eine anderen Codierung (z.B. ANSI) öffnen Wenn Sie Textdateien mit einer anderen Codierung als UTF-8 öffnen möchten, müssen Sie zum Öffnen den StreamReader-Konstruktur (also New) verwenden und die gewünschte Codierung durch ein Text.Encoding-Objekt angeben. Das Encoding-Objekt für ANSI erhalten Sie mit GetEncoding(1252). (Die Nummer 1252 bezeichnet die Codeseite für ANSI-Latin-1.) Dim enc As Text.Encoding = Text.Encoding.GetEncoding(1252) Dim sr As New IO.StreamReader("c:\test\test1.txt", enc)
Wenn Sie statt ANSI eine andere Codierung verwenden möchten, müssen Sie vorher ein entsprechendes Text.Encoding-Objekt erzeugen. Für die wichtigsten von .NET unterstützten Codierungen können Sie dabei die folgende Kurzschreibweise verwenden. (Das Ergebnis von Text.Encoding.Default ist abhängig vom Betriebssystem und von den Ländereinstellungen.) Dim enc enc enc enc enc enc
enc As Text.Encoding = Text.Encoding.ASCII = Text.Encoding.Default = Text.Encoding.UTF7 = Text.Encoding.UTF8 = Text.Encoding.Unicode = Text.Encoding.BigEndianUnicode
'US-ASCII (7 Bit) 'Windows-Default-Codepage 'UTF-7 'UTF-8 'UTF-16 'UTF-16 Big Endian
Für alle anderen Codierungen muss GetEncoding(n) oder GetEncoding("name") verwendet werden. Dabei gibt n die Nummer der Codeseite an. Alternativ kann auch deren Windows-interner Name angegeben werden (z.B. "Windows-1252"). Die folgenden Zeilen zeigen einige Beispiele: enc enc enc enc
= = = =
Text.Encoding.GetEncoding(850) Text.Encoding.GetEncoding(1252) Text.Encoding.GetEncoding(28591) Text.Encoding.GetEncoding(28605)
'OEM Multilingual Latin 1 (DOS) 'ANSI Latin-1 'ISO 8859-1 Latin-1 'ISO 8859-15 Latin-9
Eine Liste mit den am Rechner installierten Codeseiten finden Sie in der Systemsteuerung im Dialog LÄNDEREINSTELLUNGEN|ALLGEMEIN|ERWEITERT (siehe Abbildung 10.7).
Inhalt der Datei lesen Um eine Datei vollständig zu lesen, verwenden Sie die Methode ReadToEnd: Dim allLines As String allLines = sr.ReadToEnd()
Wenn Sie eine Datei zeilenweise lesen möchten, können Sie den folgenden Code als Muster verwenden (sr ist ein StreamReader-Objekt). Das Ende der Datei erkennen Sie daran, dass ReadLine als Ergebnis Nothing liefert. Seien Sie aber vorsichtig bei der Formulierung der Schleife! Wenn Sie das Schleifenende mit s = Nothing feststellen, bricht die Schleife bereits bei der ersten leeren Zeile ab! Der Test muss vielmehr IsNothing(s) oder s Is Nothing lauten (weil VB.NET Nothing und "" als gleichwertig betrachtet).
426
10 Dateien und Verzeichnisse
Abbildung 10.7: Am lokalen Rechner verfügbare Codeseiten
Dim line As String Do line = sr.ReadLine() If IsNothing(line) Then Exit Do Console.WriteLine(line) Loop
Natürlich können Sie auch einzelne Zeichen lesen. Read() liefert den Code eines Zeichens oder -1, wenn das Ende der Datei erreicht ist. (Damit auch -1 zurückgegeben werden kann, liefert Read einen Integer-Wert und nicht – wie Sie vielleicht erwarten würden – einen CharWert.) Peek funktioniert wie Read, allerdings bleibt der Dateizeiger dabei unverändert. Das Zeichen kann also anschließend mit Read gelesen werden. Die folgende Schleife schreibt jedes einzelne Zeichen einer Datei in das Konsolenfenster. While sr.Peek() -1 Console.Write(ChrW(sr.Read())) End While
Die Syntaxvariante Read(c, pos, n) ermöglicht es, n Zeichen in das Char-Feld c() zu lesen. pos gibt an, wo in c() die Zeichen eingefügt werden sollen. (pos gibt nicht die Stelle an, von der die Zeichen aus der Datei gelesen werden! Die Textdatei wird immer sequentiell gelesen.)
10.5 Textdateien lesen und schreiben
427
Read liefert die Zahl der tatsächlich gelesenen Zeichen zurück. Dieser Wert kann kleiner als n sein, wenn das Ende der Datei erreicht wird.
Datei schließen Wenn Sie die Datei fertig gelesen haben, sollten Sie darauf achten, das StreamReader-Objekt so rasch wie möglich mit Close zu schließen. Damit können andere Benutzer oder Programme wieder auf die Datei zugreifen. (Verlassen Sie sich nicht darauf, dass die Datei automatisch geschlossen wird, wenn das StreamReader-Objekt – z.B. am Ende einer Prozedur – nicht mehr gültig ist. Aufgrund der neuen Speicherverwaltung von VB.NET wird ein Objekt nicht sofort gelöscht, wenn es nicht mehr gültig ist, sondern zu einem unbestimmten Zeitpunkt!)
10.5.3 Textdateien schreiben (StreamWriter) Das Gegenstück zu StreamReader ist erwartungsgemäß die Klasse StreamWriter. Damit können Sie sowohl neue Textdateien erzeugen als auch Text an vorhandene Dateien anfügen. Per Default verwendet StreamWriter UTF-8, wobei neue Dateien nicht mit Kennzeichnungsbytes ausgestattet werden. Auf Wunsch können Sie aber natürlich auch eine andere Codierung auf der Basis eines Text.Encoding-Objekts verwenden.
Neue UTF-8-Textdateien erzeugen Es gibt eine ganze Reihe von Syntaxvarianten, um eine neue Textdatei zu erzeugen. Sollte es bereits eine Datei mit dem angegebenen Namen geben, wird diese ohne Rückfrage überschrieben (also gelöscht). Dim sw As IO.StreamWriter sw = New IO.StreamWriter("c:\test\test1.txt") sw = New IO.StreamWriter(io_stream_object) sw = IO.File.CreateText("c:\test\test2.txt") sw = New IO.FileInfo("c:\test\test3.txt").CreateText()
Vorhandene Textdatei öffnen, um Text anzufügen Wenn die Datei bereits existiert und Sie Text am Ende der Datei anfügen möchten, verwenden Sie eine der folgenden Anweisungen, um ein StreamWriter-Objekt zu erzeugen: sw = New IO.StreamWriter("c:\test\test1.txt", True) sw = IO.File.AppendText("c:\test\test2.txt") sw = New IO.FileInfo("c:\test\test3.txt").AppendText()
Andere Codierung als UTF-8 verwenden Wenn Sie eine andere Codierung als UTF-8 verwenden möchten, müssen Sie die gewünschte Codierung als dritten Parameter bei New StreamWriter angeben. (Wie Sie Text.En-
428
10 Dateien und Verzeichnisse
coding-Objekte erzeugen können, wurde bereits im vorigen Abschnitt beschrieben.) Der
zweite Parameter gibt an, ob Dateien an eine vorhandene Datei hinzugefügt werden sollen (True) oder ob die Datei neu erzeugt werden soll (False).
HINWEIS
Dim enc As Text.Encoding = Text.Encoding.GetEncoding(1252) 'ANSI sw = New IO.StreamWriter("c:\test\test1.txt", False, enc) 'neue Datei sw = New IO.StreamWriter("c:\test\test2.txt", True, enc) 'anfügen
Intern verwendet VB.NET zur Darstellung von Zeichenketten immer Unicode. Beim Speichern einer Zeichenkette in einer Textdatei werden alle Zeichen entsprechend der gewählten Codierung in Bytecodes umgewandelt. Wenn ein UnicodeZeichen in der Codierung nicht vorgesehen ist (z.B. ein deutsches Sonderzeichen bei der Codierung Text.Encoding.ASCII), wird es beim Speichern automatisch durch ein Fragezeichen ersetzt (oder durch einen speziellen Code für unbekannte Zeichen, sofern ein derartiger Code im Zeichensatz vorgesehen ist).
Kennzeichnung von Unicode-Dateien StreamWriter verwendet zwar per Default UTF-8, kennzeichnet neue Dateien aber nicht mit den drei BOM-Bytes (EF BB BF). Wenn Sie das möchten, erzeugen Sie die neue Datei unter Angabe eines Text.Encoding.UFT8-Objekts: Dim enc As Text.Encoding = Text.Encoding.UTF8 sw = New IO.StreamWriter("c:\test\test1.txt", False, enc)
UTF-16-Dateien (also Dateien auf der Basis von Text.Encoding.Unicode oder .BigEndianUnicode) werden in jedem Fall durch die zwei BOM-Bytes FF FE bzw. FE FF gekennzeichnet.
Text schreiben Mit den Methoden Write und WriteLine können Sie Text in der Datei speichern. Die Methoden unterscheiden sich nur dadurch, dass WriteLine noch ein oder zwei Bytes zur Kennzeichnung des Zeilenendes anfügt. (Diese Codes können mit NewLine ermittelt und bei Bedarf auch verändert werden. Per Default verwendet StreamWriter die unter Windows üblichen Codes 13 und 10.) Sowohl bei Write als auch bei WriteLine gibt es unzählige Syntaxvarianten, dank derer fast alle elementaren .NET-Datentypen direkt als Parameter übergeben werden können. Per Default werden bei der Umwandlung in Zeichenketten die aktuellen Ländereinstellungen berücksichtigt. sw.WriteLine(1/3)
'schreibt "0,3333333333333333" in die Datei
In der Variante Write[Line]("format", x, y ...) werden die Parameter x, y etc. entsprechend der im ersten Parameter übergebenen Zeichenkette formatiert (siehe auch Abschnitt 8.5). Write[Line] bietet allerdings keine Syntaxvariante, mit der die Landeseinstellung für die Formatierung von Zahlen und Daten verändert werden kann. Wenn Sie das möchten, müssen Sie auf String.Format zurückgreifen:
10.5 Textdateien lesen und schreiben
429
' schreibt z.B. "venerdì 4 gennaio 2002" in die Datei Dim cult As New Globalization.CultureInfo("it-IT") 'italienisch sw.WriteLine(String.Format(cult, "{0:D}", Now))
Die mit Write[Line] übergebenen Daten werden nicht sofort bei jeder Änderung physikalisch auf der Festplatte gespeichert, sondern aus Geschwindigkeitsgründen für einige Zeit zwischengespeichert. Um zu erreichen, dass die Daten tatsächlich sofort gespeichert werden, führen Sie die Methode Flush aus oder setzen die Eigenschaft AutoFlush auf True.
Datei schließen Wie bei StreamReader sollten Sie auch StreamWriter-Objekte so rasch wie möglich mit Close schließen, um die Datei für andere Programme oder Benutzer wieder freizugeben.
10.5.4 Beispiel – Textdatei erstellen und lesen Das folgende Beispielprogramm erzeugt in der Prozedur WriteTextFile zuerst das Verzeichnis C:\test (sofern dieses noch nicht existiert) und dann die Datei C:\test\test1.txt. Darin werden drei Zeilen Text gespeichert. In der Prozedur ReadTextFile wird diese Datei neu eingelesen und mit MsgBox angezeigt (siehe Abbildung 10.8). DeleteTextFile löscht schließlich (nach einer Rückfrage) das gesamte Verzeichnis C:\test.
Abbildung 10.8: Die Dialogbox zeigt den Inhalt der Datei c:\test\test1.txt
Wenn Sie ein Programm zur hexadezimalen Darstellung von Dateien besitzen, können Sie sich Byte für Byte ansehen, wie die Datei intern aussieht (siehe Abbildung 10.9 mit dem Freeware-Hexeditor XVI32). Deutlich zu sehen sind die drei UTF-8-Kennzeichnungsbytes sowie die Codierung der Zeichen äöü bzw. € durch zwei bzw. drei Bytes. Sowohl Write- als auch ReadTextFile verwenden dasselbe Text.Encoding-Objekt zur Codierung des Texts. Versuchen Sie probeweise, die erste Codezeile in Main so zu verändern, dass die ASCII-Codierung verwendet wird (Text.Encoding.ASCII). Sie werden feststellen, dass die deutschen Sonderzeichen und das Eurozeichen dann fehlen. Die Datei enthält stattdessen Fragezeichen.
430
10 Dateien und Verzeichnisse
Abbildung 10.9: Ansicht von c:\test\test1.txt mit einem Hexeditor
' Beispiel dateien\textdateien Dim enc As Text.Encoding Sub Main() enc = Text.Encoding.UTF8 WriteTextFile() ReadTextFile() DeleteTextFile() End Sub
HINWEIS
Sub WriteTextFile() ' Verzeichnis c:\test erzeugen Dim dir As New IO.DirectoryInfo("c:\test") If Not dir.Exists Then dir.Create() ' neue Textdatei c:\test\test1.txt erzeugen Dim sw As IO.StreamWriter sw = New IO.StreamWriter("c:\test\test1.txt", False, enc) sw.WriteLine("bla bla äöü €") sw.WriteLine("zweite Zeile") sw.WriteLine("dritte Zeile") sw.Close() End Sub
Beachten Sie bitte, dass das Programm ohne die obige Anweisung sw.Close nicht funktionieren würde! Zwar verliert die Variable sw am Ende der Prozedur ihre Gültigkeit. Das StreamWriter-Objekt wird aber erst bei der nächsten garbage collection tatsächlich aus dem Speicher entfernt (und Sie wissen nicht, wann diese stattfindet). Bis dahin gilt die Datei als offen. In ReadTextFile würde daher beim Versuch, die Datei zu öffnen, ein Fehler auftreten!
10.5 Textdateien lesen und schreiben
431
Sub ReadTextFile() ' Textdatei c:\test\test1.txt lesen Dim sr As New IO.StreamReader("c:\test\test1.txt", enc) MsgBox(sr.ReadToEnd) sr.Close() End Sub Sub DeleteTextFile() ' Verzeichnis c:\test löschen If MsgBox("Soll das Verzeichnis c:\test vollständig gelöscht " & _ "werden?", MsgBoxStyle.YesNo) = MsgBoxResult.Yes Then IO.Directory.Delete("c:\test", True) End If End Sub
10.5.5 Beispiel – Textcodierung ändern Sie können die StreamReader- und StreamWriter-Klasse auch dazu verwenden, um die Codierung einer Textdatei zu ändern. Dazu verwenden Sie ein StreamReader-Objekt, um die Datei zu lesen, und ein StreamWriter-Objekt, um die Datei unter einem anderen Namen zu speichern. Die folgende Prozedur demonstriert die Vorgehensweise anhand einer Konvertierung von UTF-8 (enc1) nach ANSI (enc2). Beachten Sie bitte, dass dabei Unicode-Zeichen, die im ANSI-Zeichensatz nicht vorgesehen sind, durch Fragezeichen ersetzt werden! ' Beispiel dateien\textdateien Sub ConvertTextFile() Dim enc1 As Text.Encoding = Text.Encoding.UTF8 Dim enc2 As Text.Encoding = Text.Encoding.GetEncoding(1252) Dim sr As New IO.StreamReader("c:\test\test1.txt", enc1) Dim sw As New IO.StreamWriter("c:\test\test2.txt", False, enc2) sw.Write(sr.ReadToEnd()) sw.Close() sr.Close() End Sub
Die Anweisung sw.Write(sr.ReadToEnd()) ist zwar für den Programmierer praktisch, aber in der Ausführung nicht unbedingt optimal: Die zu konvertierende Datei wird dadurch vollständig in den Arbeitsspeicher geladen. Das funktioniert nur bei kleinen Dateien gut. Bei großen Dateien muss die Datei dagegen stückweise gelesen und geschrieben werden. Die folgenden Zeilen zeigen dafür einen entsprechenden Algorithmus, der ein Char-Feld als Pufferspeicher verwendet. Mit Read werden bis zu 65536 Zeichen in dieses Feld gelesen. Mit Write werden anschließend die tatsächlich gelesenen Zeichen (Variable n) gespeichert.
432
10 Dateien und Verzeichnisse
Sub ConvertTextFile() Dim enc1, enc2, sr, sw ... wie oben Const buffersize As Integer = 65536 Dim buffer(buffersize) As Char Dim n As Integer Do n = sr.Read(buffer, 0, buffersize) sw.Write(buffer, 0, n) Loop Until n < buffersize sw.Close() sr.Close() End Sub
10.5.6 Zeichenketten lesen und schreiben (StringReader und StringWriter) Zu den in den vorigen Abschnitten vorgestellten Klassen StreamReader und -Writer gibt es zwei analoge Klassen StringReader und -Writer. Der wesentliche Unterschied besteht darin, dass Sie damit nicht Dateien, sondern Zeichenketten lesen oder schreiben können. Beispielsweise können Sie diese Klassen dazu verwenden, um Daten, die Sie sonst in einer temporären Datei zwischenspeichern würden, in einer String-Variablen zwischenzuspeichern. Der offensichtliche Vorteil besteht darin, dass dazu kein Festplattenzugriff erforderlich ist – die Vorgehensweise ist daher (zumindest bei kleinen Datenmengen) deutlich effizienter. Gleichzeitig sind nur minimale Änderungen am Code erforderlich, weil die meisten Eigenschaften und Methoden äquivalent sind.
StringReader Mit den folgenden zwei Zeilen wird zuerst eine Zeichenkette initialisiert und auf deren Basis dann das StringReader-Objekt strr erzeugt. Aus diesem Objekt können Sie nun wie bei einem StreamReader-Objekt mit den Methoden Read, ReadLine oder ReadToEnd einzelne Zeichen, Zeilen oder den gesamten Inhalt lesen. Beachten Sie, dass das StringReader-Objekt statisch ist. Eine nachträgliche Veränderung der Variablen s hat keinerlei Einfluss mehr auf dieses Objekt! Dim s As String = "abcdefg" + vbCrLf + "zweite Zeile" + vbCrLf Dim strr As New IO.StringReader(s) Console.WriteLine(strr.ReadLine())
StringWriter Einem StringWriter-Objekt liegt intern ein (bereits in Abschnitt 8.2.6 vorgestelltes) StringBuilder-Objekt zugrunde. Insofern bietet die Klasse StringWriter einfach eine weitere Möglichkeit, Zeichenketten sehr viel effizienter als durch s = s + "abc" zusammenzusetzen. Dazu verwenden Sie die Methoden Write oder WriteLine.
10.5 Textdateien lesen und schreiben
433
Dim strw As New IO.StringWriter() strw.WriteLine("erste Zeile") strw.WriteLine("zweite Zeile")
Sie können vom StringWriter-Objekt jederzeit mit den Methoden GetStringBuilder oder ToString ein StringBuilder-Objekt oder eine gewöhnliche Zeichenkette ableiten. Console.Write(strw.ToString)
Wie bei einem StreamWriter-Objekt können Sie das Schreiben mit Close abschließen. Das bedeutet, das weitere Veränderungen nicht mehr möglich sind. Der Inhalt der StringWriterObjekts bleibt aber erhalten, d.h., GetStringBuilder und ToString funktionieren weiterhin. Wenn Sie den Inhalt des StringWriter-Objekts dagegen löschen möchten, müssen Sie die Methode Dispose ausführen.
10.5.7 Syntaxzusammenfassung Codierungs-Objekt erzeugen System.Text.Encoding-Objekt erzeugen Dim enc As Text.Encoding enc = Text.Encoding.ASCII enc = Text.Encoding.UTF7 enc = Text.Encoding.UTF8 enc = Text.Encoding.Unicode enc = Text.Encoding.GetEncoding(n)
erzeugt ein Text.Encoding-Objekt für den angegebenen Zeichensatz.
Textdateien lesen System.IO.StreamReader-Objekt erzeugen New IO.StreamReader("name.txt")
öffnet die Textdatei.
New IO.StreamReader("name.txt", enc)
wie oben, aber unter Anwendung eines bestimmten Zeichensatzes.
IO.File.OpenText("name.txt")
öffnet die Textdatei.
New IO.FileInfo("name.txt").OpenText()
öffnet ebenfalls die Textdatei.
System.IO.StreamReader-Klasse – Textdatei lesen Close()
schließt die Datei.
CurrentEncoding
liefert ein Text.Encoding-Objekt, das die Codierung angibt.
434
10 Dateien und Verzeichnisse
System.IO.StreamReader-Klasse – Textdatei lesen Peek()
liefert das nächste Zeichen und liefert dessen Code oder -1, ohne den Dateizeiger weiter zu bewegen.
Read()
liest das nächste Zeichen und liefert dessen Code oder -1.
Read(c, 0, n)
liest maximal n Zeichen in das Char-Feld c.
ReadLine()
liefert die nächste Zeile (oder Nothing, wenn das Dateiende erreicht ist).
ReadToEnd()
liefert den gesamten Text bis zum Ende der Datei.
Textdateien schreiben System.IO.StreamWriter-Objekt erzeugen New IO.StreamWriter("name.txt")
erzeugt eine neue Textdatei.
New IO.StreamWriter("name.txt", False, enc)
wie oben, aber unter Anwendung eines bestimmten Zeichensatzes.
New IO.FileInfo("name.txt").CreateText()
erzeugt eine neue Textdatei.
New IO.StreamWriter(io_stream_object)
leitet das Objekt von IO.Stream ab.
New IO.StreamWriter("name.txt", True)
ergänzt eine vorhandene Textdatei.
New IO.StreamWriter("name.txt", True, enc)
wie oben, aber unter Anwendung eines bestimmten Zeichensatzes.
IO.File.AppendText("name.txt")
ergänzt eine vorhandene Textdatei.
New IO.FileInfo("name.txt").AppendText()
ergänzt eine vorhandene Textdatei.
System.IO.StreamWriter-Klasse – Textdateien schreiben Close()
schließt die Datei.
Flush()
speichert alle Änderungen auf der Festplatte.
NewLine
gibt die Zeichen zum Zeilenwechsel an bzw. verändert sie.
Write[Line](x)
speichert den Inhalt von x im Textformat in der Datei.
Write[Line](c, pos, n)
speichert n Zeichen beginnend bei der Position pos aus dem Char-Feld c in der Datei.
10.6 Binärdateien lesen und schreiben
10.6
435
Binärdateien lesen und schreiben
Die im vorigen Abschnitt besprochenen Textdateien haben den Vorteil, dass sie (einen geeigneten Editor vorausgesetzt) problemlos gelesen werden können. Der Inhalt einer Datei kann daher auch ohne ein Spezialprogramm leicht festgestellt werden. Bei Binärdateien werden Daten dagegen in ihrer internen Darstellung gespeichert. Beispielsweise wird eine Double-Fließkommazahl nicht mehr in der Form "123.456789012345" gespeichert, sondern durch die hexadezimalen Codes 69 4C FB 07 3C DD 5E 40. Das hat Vor- und Nachteile: Auf der einen Seite sind Binärdateien meist deutlich kleiner. Zudem ist das Lesen und Schreiben von Daten effizienter, weil keine Umwandlungen zwischen unterschiedlichen Darstellungsformaten erforderlich sind. Auf der anderen Seite können derartige Dateien nur noch mit einem Programm gelesen werden, das den exakten internen Aufbau der Datei kennt. Für den Austausch von Daten zwischen unterschiedlichen Programmen ist das oft nicht förderlich. Zum Lesen und Schreiben von Binärdateien bietet der System.IO-Namensraum zwei prinzipielle Möglichkeiten: •
Die FileStream-Klasse ermöglicht einen Dateizugriff auf unterster Ebene. Mit davon abgeleiteten Objekten können Sie einzelne Bytes (oder ganze Gruppen von Bytes) lesen und schreiben. Für die Interpretation der Daten sind Sie selbst verantwortlich. Die FileStream-Klasse bietet eine fast vollständige Kontrolle über den Lese- bzw. Schreibprozess: Sie können die Lese- bzw. Schreibposition innerhalb der Datei jederzeit verändern und so die Daten in beliebiger Reihenfolge lesen oder schreiben, Sie können in derselben Datei lesen und schreiben, Sie können zwischen synchronem und asynchronem Zugriff wählen (siehe Abschnitt 10.7) etc.
•
Die beiden Klassen BinaryReader und -Writer ermöglichen es, die meisten .NET-Basisdatentypen unmittelbar zu lesen und schreiben. Beispielsweise können Sie damit eine Double-Variable speichern, ohne sich Gedanken über die interne Darstellung dieser Variablen zu machen. Dieser Vorteil wird allerdings durch andere Einschränkungen gegenüber der FileStream-Klasse erkauft.
Daneben beschreibt dieser Abschnitt noch zwei weitere Klassen, die mit der FileStreamKlasse verwandt sind: •
MemoryStream bietet im Wesentlichen dieselben Methoden und Eigenschaften wie FileStream, der Datenstrom wird aber im Arbeitsspeicher verwaltet. MemoryStream eignet
sich daher vor allem als Ersatz für temporäre Dateien. (Es ist möglich, ein vorhandenes MemoryStream-Objekt zu einem späteren Zeitpunkt mit einem FileStream-Objekt zu
speichern.) •
BufferedStream ist eine Hilfsklasse für alle von IO.Stream abgeleiteten Klassen. Die Klasse
hilft dabei, wiederholte Lese- bzw. Schreibzugriffe durch die Verwendung eines (größeren) Zwischenspeichers zu optimieren.
436
10 Dateien und Verzeichnisse
10.6.1 FileStream Als Stream wird in der Informatik generell ein Fluss von Daten bezeichnet, ganz unabhängig davon, woher oder wohin die Daten fließen. Es verwundert deswegen nicht, dass es in der .NET-Bibliothek ein allgemeines IO.Stream-Objekt gibt, von dem dann FileStream abgeleitet ist, um den Datenfluss aus oder in Dateien zu steuern. Von Stream ist aber z.B. auch Net.Sockets.NetworkStream abgeleitet (Datenfluss im Netzwerk).
FileStream-Objekt erzeugen Der New-Konstruktor sieht gleich eine ganze Reihe von Syntaxvarianten vor, um ein neues FileStream-Objekt zu erzeugen: Dim fs As IO.FileStream fs = New IO.FileStream("filename", fs = New IO.FileStream("filename", fs = New IO.FileStream("filename", fs = New IO.FileStream("filename",
modus) modus, access) modus, access, sharing) modus, access, sharing, buffersize)
modus ist ein Element der IO.FileMode-Aufzählung. Der Parameter gibt an, ob eine vorhandene Datei geöffnet oder eine neue Datei erzeugt werden soll (z.B. IO.FileMode.Create, Open, OpenOrCreate, Append etc.). access ist ein Element der IO.FileAccess-Aufzählung. Der Parameter bestimmt, ob die Datei für den Lese- und/oder Schreibzugriff geöffnet werden soll (IO.FileAccess.Read, Write oder ReadWrite). Per Default wird die Datei zum Lesen und Schreiben geöffnet. Wenn Sie die Datei nicht verändern brauchen, sollten Sie unbedingt Read angeben, um Zugriffskonflikte
mit anderen Programmen möglichst zu vermeiden. sharing ist ein Element der IO.FileShare-Aufzählung. Der Parameter bestimmt, ob und wie
andere Programme auf dieselbe Datei zugreifen dürfen (während die Datei von Ihrem Programm noch geöffnet ist): Die Defaulteinstellung IO.FileShare.None blockiert die Datei für jeden anderen Zugriff. IO.FileShare.Read erlaubt anderen Prozessen, die Datei zu lesen, sie aber nicht zu verändern. IO.FileShare.Write bzw. ReadWrite erlauben auch einen schreibenden Zugriff. (Experimente mit diesem Parameter haben allerdings gezeigt, dass dieser nicht immer so funktioniert, wie er dokumentiert ist. Weitere Informationen folgen gleich unter der Überschrift Sharing-Probleme.) buffersize gibt schließlich die Größe des Puffers an, der Dateizugriffe zwischenspeichert. Die
VERWEIS
Defaultgröße des Puffers ist nicht dokumentiert und möglicherweise auch betriebssystemabhängig. Die Vorgabe einer eigenen Puffergröße ist nur in Ausnahmefällen sinnvoll. FileStream-Objekte können auch mit den Methoden Create, Open, OpenRead und OpenWrite der Objekte IO.File und IO.FileInfo erzeugt werden. Eine Syntaxzusammenfas-
sung dieser Methoden finden Sie in Abschnitt 10.3.8.
10.6 Binärdateien lesen und schreiben
437
Daten lesen und schreiben Es gibt nur zwei Methoden, um Daten aus einem FileStream zu lesen: ReadByte() liest ein einzelnes Byte. Die Methode liefert als Ergebnis allerdings einen Integer-Wert – entweder den Wert des Datenbytes oder -1, wenn das Ende der Datei erreicht ist. Die Methode Read ermöglicht es, mehrere Bytes auf einmal in eine beliebige Position innerhalb eines ByteFelds zu lesen. Diese Methode liefert die Anzahl der gelesenen Bytes zurück. (Sie erkennen das Dateiende daran, dass weniger Bytes gelesen als angefordert wurden.) Auch die Methoden zum Schreiben von Daten sind spartanisch: WriteByte schreibt ein einzelnes Byte. Write speichert eine angegebene Anzahl von Bytes aus einem Byte-Feld. Mit beiden Write-Methoden können auch vorhandene Daten einer Datei überschrieben werden.
Dateizeiger verändern Zu den besonderen Eigenschaften der FileStream-Klasse zählt die Möglichkeit, die Position des Dateizeigers zu verändern. (Der Dateizeiger zeigt auf das nächste Byte, das gelesen bzw. geschrieben oder überschrieben wird.) Die aktuelle Position kann mit Position ermittelt werden. (Die Zählung beginnt wie üblich bei 0. Diese Position nimmt der Dateizeiger normalerweise auch unmittelbar nach dem Öffnen einer Datei ein. Die einzige Ausnahme besteht dann, wenn die Datei mit modus = IO.FileMode.Append geöffnet wurde. In diesem Fall zeigt der Dateizeiger auf das Ende der Datei.) Mit Seek kann die Position verändert werden. An die Methode werden zwei Parameter übergeben: Der erste Parameter gibt an, um wie viele Bytes der Zeiger verstellt werden soll. Dieser Wert darf auch negativ sein. Der zweite Parameter muss ein Element der IO.SeekOrigin-Aufzählung sein. Der Wert bestimmt die Startposition für die Bewegung. Die drei Möglichkeiten sind: Dateianfang, Dateiende oder die aktuelle Position. fs.Seek(0, IO.SeekOrigin.Begin) fs.Seek(3, IO.SeekOrigin.Current) fs.Seek(-3, IO.SeekOrigin.End)
'Dateizeiger zurück an den Beginn 'Dateizeiger drei Bytes vorbewegen 'Dateizeiger drei Bytes vor 'das Dateiende bewegen
Die Länge der Datei (d.h. die größtmögliche Position des Dateizeigers) kann mit Length ermittelt werden.
Änderungen ausführen, Datei schließen Wenn Sie Daten mit Write[Byte] speichern bzw. verändern, werden diese Änderungen aus Effizienzgründen nicht sofort physikalisch auf der Festplatte durchgeführt. Sie können eine sofortige Speicherung aber jederzeit mit der Methode Flush erreichen. Wie bei den anderen in diesem Kapitel vorgestellten IO-Objekten zum Dateizugriff sollten Sie auch bei FileStream darauf achten, das Objekt so rasch wie möglich mit Close zu schließen. Damit können andere Benutzer oder Programme wieder auf die Datei zugreifen. (Verlassen Sie sich nicht darauf, dass die Datei automatisch geschlossen wird, wenn das Objekt – z.B. am Ende einer Prozedur – nicht mehr gültig ist. Aufgrund der neuen
438
10 Dateien und Verzeichnisse
Speicherverwaltung von VB.NET wird ein Objekt nicht sofort gelöscht, sondern zu einem unbestimmten Zeitpunkt!)
Beispiel – Binärdatei lesen und schreiben Das folgende Beispielprogramm ermittelt mit IO.Path.GetTempFileName einen temporären Dateinamen, erzeugt diese Datei und speichert dort zehn Bytes. Abbildung 10.10 zeigt den Inhalt der Datei im Hex-Editor XVI32. Der Dateizeiger wird nun zum ersten Mal zurück an den Beginn gestellt, um die Daten mit einer Schleife Byte für Byte auszulesen. Anschließend wird der Dateizeiger ein zweites Mal zurückgestellt, um die Daten nun auf einmal in ein Byte-Feld zu übertragen.
Abbildung 10.10: Der Inhalt der binären Testdatei
' Beispiel dateien\filestream Sub main() Dim i As Byte Dim data As Integer Dim bytes() As Byte Dim filename As String = IO.Path.GetTempFileName Dim fs As New IO.FileStream(filename, IO.FileMode.Create) ' 10 Bytes in der Datei speichern For i = 0 To 9 fs.WriteByte(i) Next ' Dateizeiger zurück an den Anfang der Datei ' Daten Byte für Byte lesen, bis das Ende der Datei erreicht wird fs.Seek(0, IO.SeekOrigin.Begin)
10.6 Binärdateien lesen und schreiben
439
Do data = fs.ReadByte() If data = -1 Then Exit Do Console.Write(data & " ") Loop Console.WriteLine() ' Dateizeiger nochmals zurück an den Anfang der Datei ' alle Daten als Byte-Block lesen fs.Seek(0, IO.SeekOrigin.Begin) ReDim bytes(CInt(fs.Length - 1)) fs.Read(bytes, 0, CInt(fs.Length)) For Each i In bytes Console.Write(i & " ") Next Console.WriteLine() ' Datei schließen, Programmende fs.Close() Console.WriteLine("Drücken Sie Return") Console.ReadLine() Debug.WriteLine(filename) End Sub
Sharing-Probleme (gemeinsamer Zugriff auf Dateien) Wenn man der Dokumentation zu IO.FileSharing-Aufzählung folgt, sollte es eigentlich kein Problem sein, mit zwei FileStream-Objekten auf dieselbe Datei zuzugreifen. (Das wird in der Praxis selten erforderlich sein. Auf diese Weise kann aber rasch mit nur einem Programm getestet werden, ob der gemeinsame Zugriff mehrerer Programme oder Benutzer auf eine Datei prinzipiell funktioniert.) Mit den folgenden Zeilen sollte mit fs1 eine temporäre Datei erzeugt und beschrieben werden. Mit fs2 sollte dann die Datei gelesen werden (ohne vorher fs1 zu schließen.) Das Experiment scheiterte allerdings schon in Zeile fs2 = New IO.FileStream(...) mit einer Fehlermeldung (IO.IOException) wegen eines Konflikts beim Dateizugriff. Dim i As Byte Dim fs1, fs2 As IO.FileStream Dim filename As String = IO.Path.GetTempFileName ' fs1 erzeugt die Datei und schreibt zehn Bytes fs1 = New IO.FileStream(filename, IO.FileMode.Create, _ IO.FileAccess.Write, IO.FileShare.Read) For i = 0 To 9 : fs1.WriteByte(i) : Next
440
10 Dateien und Verzeichnisse
' nun mit fs2 dieselbe Datei lesen fs2 = New IO.FileStream(filename, IO.FileMode.Open, _ IO.FileAccess.Read, IO.FileShare.Read) ...
Mit einem zweiten Test wird zuerst eine Datei mit dem FileStream-Objekt fs1 erzeugt. fs1 wird anschließend geschlossen. Danach versuchen zwei weitere FileStream-Objekte fs2 und fs3 gleichzeitig, diese Datei zu lesen. Das klappt – allerdings nur dann, wenn beide Objekte mit access = IO.FileAccess.Read geöffnet wurden. Dim filename As String = IO.Path.GetTempFileName Dim fs1, fs2, fs3 As IO.FileStream Dim i As Byte Dim data As Integer ' fs1 erzeugt die Datei und schreibt zehn Bytes; ' die Datei wird danach geschlossen fs1 = New IO.FileStream(filename, IO.FileMode.Create) For i = 0 To 9 : fs1.WriteByte(i) : Next fs1.Close() ' fs2 öffnet die Datei im Lesezugriff fs2 = New IO.FileStream(filename, IO.FileMode.Open, _ IO.FileAccess.Read) Do data = fs2.ReadByte() If data = -1 Then Exit Do Console.Write(data & " ") Loop Console.WriteLine() ' fs3 öffnet die Datei ebenfalls fs3 = New IO.FileStream(filename, IO.FileMode.Open, _ IO.FileAccess.Read) Do data = fs3.ReadByte() If data = -1 Then Exit Do Console.Write(data & " ") Loop Console.WriteLine()
Fazit: Auch wenn die Dokumentation zu IO.FileShare.Write bzw. ReadWrite vermuten lässt, dass ein gleichzeitiger Schreibzugriff durch zwei Prozesse möglich ist, ist dies in der Praxis nicht (zumindest nicht immer) der Fall. Ein gleichzeitiger Lesezugriff ist möglich, erfordert aber nicht die Angabe eines sharing-Parameters.
10.6 Binärdateien lesen und schreiben
441
Lock und Unlock Die FileStream-Klasse sieht die Methoden Lock und Unlock vor, um einen Bytebereich einer Datei vorübergehend für jeden Zugriff durch andere Prozesse zu blockieren. Nachdem ein gemeinsamer Schreibzugriff nicht gelungen ist (siehe oben), wurden die beiden Methoden nur im Hinblick auf einen gemeinsamen Lesezugriff getestet. Dabei hat sich gezeigt, dass der Zugriff mit den Methoden zwar tatsächlich gesteuert werden kann, dass die durch Lock ausgelöste Blockierung aber für die gesamte Datei oder zumindest für einen größeren Bereich der Datei gilt (eventuell für die Puffergröße), nicht nur für den angegebenen Bytebereich. fs1 = New IO.FileStream(filename, IO.FileMode.Open, _ IO.FileAccess.Read) fs1.Lock(5, 2) 'blockiert zwei Bytes ab der Position 5 ' das folgende Kommando löst einen Zugriffsfehler aus, ' obwohl die ersten Bytes eigentlich nicht blockiert sind fs2 = New IO.FileStream(filename, IO.FileMode.Open, _ IO.FileAccess.Read)
Fazit: Die Parameter von Lock und Unlock gaukeln eine Genauigkeit bei der Blockierung von Dateien vor, die in der Praxis nicht erzielbar ist. Stattdessen gilt das Motto: Alles oder nichts.
10.6.2 BufferedStream (FileStream beschleunigen) Die BufferedStream-Klasse hilft, die Effizienz beim Umgang mit anderen IO.Stream-Objekten in bestimmten Fällen zu steigern. Dazu werden Schreibvorgänge zuerst im Arbeitsspeicher zwischengespeichert, bevor sie tatsächlich durchgeführt werden. Bei Lesevorgängen werden entsprechend schon im Voraus große Datenmengen gelesen, die dann sofort zur Verfügung stehen. Die Anwendung eines BufferedStream empfiehlt sich laut Dokumentation dann, wenn sehr viele Lese- oder Schreibvorgänge hintereinander durchgeführt werden, es aber nur selten zu einem Wechsel zwischen Lese- und Schreibvorgängen kommt. Zum Erzeugen eines BufferedStream-Objekts muss ein anderes, bereits existierendes IO.Stream-Objekt (oder ein Objekt einer davon abgeleiteten Klasse) im New-Konstruktor angegeben werden. Diese Vorgehensweise bietet sich natürlich besonders bei FileStreamObjekten an. (Bei einem MemoryStream können Sie durch einen Zwischenpuffer nichts beschleunigen, weil sich die Daten ohnedies schon im Arbeitsspeicher befinden.) Anschließend wenden Sie die Read- oder Write-Methoden auf das BufferedStream-Objekt anstatt auf Ihr ursprüngliches Objekt an. Die Verwendung des BufferedStream-Objekts erfordert also fast keine Veränderung an bereits bestehendem FileStream-Code. Dim fs As New IO.FileStream(...) Dim bs As New IO.BufferedStream(fs) bs.WriteByte(...) ... fs.Close() 'schließt sowohl fs als auch bs
442
10 Dateien und Verzeichnisse
Beispiel Natürlich werden auch bei normalen FileStream-Objekten Lese- und Schreibvorgänge zwischengespeichert. (Es wäre unglaublich langsam, wenn bei jedem einzelnen Byte, das gelesen oder geschrieben wird, ein Festplattenzugriff erforderlich wäre.) Insofern hatte ich ursprünglich gewisse Zweifel, ob die Verwendung eines BufferedStream-Objektes tatsächlich eine Geschwindigkeitssteigerung bringen würde. Das folgende Testprogramm (siehe Abbildung 10.11) beweist aber, dass tatsächlich eine Zugriffsoptimierung stattfindet. Das Programm erzeugt zwei ca. 10 MByte große temporäre Dateien. Die erste Datei wird auf der Basis eines FileStream-Objekts erstellt, die zweite unter Zuhilfenahme eines BufferedStream-Objekts. In beiden Fällen werden die Daten Byte für Byte mit WriteByte gespeichert. Bei diesem einfachen Test ist die BufferedStream-Variante um ca. 25 Prozent schneller.
Abbildung 10.11: Geschwindigkeitsvergleich zwischen FileStream und BufferedStream
' Beispiel dateien\bufferedstream Sub main() Dim starttime As Date Dim time1, time2 As TimeSpan Const nr_of_bytes As Integer = 10000000 Dim i As Integer ' Daten ohne BufferedStream speichern Dim fs1 As New IO.FileStream(IO.Path.GetTempFileName(), _ IO.FileMode.Create) starttime = Now For i = 0 To nr_of_bytes - 1 fs1.WriteByte(CByte(i Mod 16)) Next fs1.Close() time1 = Now.Subtract(starttime) ' Daten mit BufferedStream speichern Dim fs2 As New IO.FileStream(IO.Path.GetTempFileName(), _ IO.FileMode.Create) starttime = Now Dim bs As New IO.BufferedStream(fs2) For i = 0 To nr_of_bytes - 1 bs.WriteByte(CByte(i Mod 16)) Next
10.6 Binärdateien lesen und schreiben
443
fs2.Close() ' damit ist auch bs geschlossen! time2 = Now.Subtract(starttime) ' Ergebnis anzeigen, temporäre Dateien löschen Console.WriteLine("FileStream: {0} Sekunden", time1) Console.WriteLine("BufferedStream: {0} Sekunden", time2) IO.File.Delete(fs1.Name) IO.File.Delete(fs2.Name) Console.WriteLine("Drücken Sie Return") Console.ReadLine() End Sub
10.6.3 MemoryStream (Streams im Arbeitsspeicher) MemoryStream verwaltet einen Stream im Arbeitsspeicher. Da die Klasse ebenfalls von IO.Stream abgeleitet ist, stehen die meisten von FileStream bekannten Eigenschaften und Methoden auch für MemoryStream-Objekte zur Verfügung.
Im einfachsten Fall, d.h., wenn das Objekt mit New IO.MemoryStream() erzeugt wurde, erfolgt die Speicherverwaltung automatisch. Die zahlreichen New-Konstruktor-Varianten ermöglichen es aber auch, in der Größe oder im Inhalt unveränderliche Streams auf der Basis von Byte-Feldern zu erzeugen. Dim ms As New IO.MemoryStream() ms.WriteByte(...)
Mit WriteTo können Sie den Inhalt eines MemoryStream-Objekts in ein anderes Stream-Objekt übertragen. Wenn Sie dabei als Parameter ein FileStream-Objekt angeben, können Sie ein MemoryStream-Objekt in einer Datei speichern. Mit den folgenden Zeilen wird zuerst ein MemoryStream-Objekt erzeugt, um darin zehn Bytes zu speichern. Anschließend wird der Inhalt des Objekts in einer temporären Datei gespeichert. Dim i As Integer Dim ms As New IO.MemoryStream() For i = 1 To 10 ms.WriteByte(CByte(i)) Next Dim fs As New IO.FileStream(IO.Path.GetTempFileName(), _ IO.FileMode.Create) ms.WriteTo(fs)
Wenn Sie das gesamte MemoryStream-Objekt in einer anderen Form weiterbearbeiten möchten, können Sie es mit der Methode ToArray in ein Byte-Feld kopieren.
444
10 Dateien und Verzeichnisse
10.6.4 BinaryReader und -Writer (Variablen binär speichern) Mit Objekten der beiden Binary-Klassen können Sie binäre Daten lesen oder schreiben. Der wesentliche Vorteil gegenüber FileStream besteht in den reichhaltigen Read- und WriteMethoden: Mit BinaryWriter.Write können Sie die meisten elementaren .NET-Datentypen direkt in einer Datei speichern (darunter Byte, Short, Integer, Long, Single, Double, Decimal sowie Char und String). Anders als bei FileStream brauchen Sie sich also nicht mehr mit einzelnen Bytes zu plagen, sondern können ganze Variablen unmittelbar speichern bzw. lesen.
TIPP
BinaryReader bietet eine ganze Kollektion entsprechender ReadXxx-Methoden, z.B. ReadByte, ReadInt16, ReadInt32 etc. Falls während des Leseversuchs das Ende der Datei erreicht wird, kommt es zu einem EndOfStream-Fehler.
Aus schwer nachvollziehbaren Gründen wird der elementare Datentyp Date von den Binary-Objekten nicht unterstützt. Die einfachste Abhilfe besteht darin, d.Ticks zu speichern (das ist ein Long-Wert) und später ReadInt64 zu verwenden, um die Daten wieder einzulesen. Das Beispielprogramm demonstriert diese Vorgehensweise.
Die BinaryReader- bzw. BinaryWriter-Klassen sind zwar in der Objekthierarchie nicht von der Klasse IO.FileStream abgeleitet, verwenden aber intern ein derartiges Objekt. Aus diesem Grund muss vor der Erzeugung eines BinaryReader- oder BinaryWriter-Objekts zuerst ein FileStream-Objekt zur Verfügung stehen. Dim fs As New IO.FileStream("name.bin", IO.FileMode.Create) Dim bw As New IO.BinaryWriter(fs) bw.Write(x) ...
Im Gegensatz zu FileStream-Objekten können Sie mit BinaryXxx-Objekten nicht wechselweise Daten lesen und schreiben. Eine weitere Einschränkung gilt nur für BinaryReader: Es ist nicht möglich, die Leseposition (den Dateizeiger) durch Seek zu verändern.
Umgang mit Zeichenketten BinaryReader und -Writer berücksichtigen beim Lesen bzw. Schreiben von Char- und StringVariablen die Codierung, die beim Erzeugen des BinaryWriter-Objekts angegebenen wurde. Wenn die Codierung nicht angegeben wird, verwendet BinaryWriter per Default
UTF8. Das bedeutet, dass je nach Zeichen unterschiedlich viele Bytes zur Codierung verwendet werden!
10.6 Binärdateien lesen und schreiben
445
Dim enc As Text.Encoding = New Text.UnicodeEncoding() Dim fs As New IO.FileStream(filename, IO.FileMode.Create) Dim bw As New IO.BinaryWriter(fs, enc) BinaryWriter speichert zudem bei String-Variablen die Anzahl der Zeichen. Das hilft BinaryReader später, die richtige Länge der Zeichenkette zu erkennen. Die Zeichenanzahl wird
dabei auf eine besondere Weise codiert: Werte bis zu 127 werden in einem einzigen Byte gespeichert. Größere Werte werden auf mehrere Bytes verteilt, wobei in jedem Byte nur sieben Bits für die Codierung der Zahl genutzt werden. Das achte Bit gibt an, ob im nächsten Byte weitere Bits folgen. Auf diese Weise können beliebig große ganze Zahlen gespeichert werden, ohne jedes Mal vier Bytes zu beanspruchen.
Beispielprogramm Das folgende Beispielprogramm erzeugt mit IO.Path.GetTempFileName einen temporären Dateinamen. Dieser Name wird zuerst dazu benutzt, um eine neue Datei zu erzeugen und darin eine Double-Zahl, zwei Zeichenketten und die aktuelle Zeit zu speichern. Anschließend werden diese Daten wieder aus der Datei extrahiert und mit MsgBox zur Kontrolle angezeigt. Abbildung 10.12 zeigt den Inhalt der temporären Datei im Hex-Editor XVI32. Es ist offensichtlich, dass der Inhalt dieser Datei nur noch für den Computer verständlich ist.
Abbildung 10.12: Der Inhalt der temporären Datei, dargestellt in einem Hex-Editor
' Beispiel dateien\binaryreader Sub Main() Dim filename As String = IO.Path.GetTempFileName WriteBinaryData(filename) ReadBinaryData(filename) End Sub
446
10 Dateien und Verzeichnisse
Sub WriteBinaryData(ByVal filename As String) Dim x As Double = 1 / 7 Dim s1 As String = "abcäöü€" Dim s2 As String = "blabla" Dim d As Date = Now Dim fs As New IO.FileStream(filename, IO.FileMode.Create) Dim bw As New IO.BinaryWriter(fs) bw.Write(x) bw.Write(s1) bw.Write(s2) bw.Write(d.Ticks) bw.Close() End Sub Sub ReadBinaryData(ByVal filename As String) Dim x As Double, s1, s2 As String Dim fs As New IO.FileStream(filename, IO.FileMode.Open) Dim br As New IO.BinaryReader(fs) Dim d As Date x = br.ReadDouble() s1 = br.ReadString() s2 = br.ReadString() d = New Date(br.ReadInt64()) MsgBox("x=" & x & vbCrLf & _ "s1=" & s1 & vbCrLf & _ "s2=" & s2 & vbCrLf & _ "d=" & d) br.Close() End Sub
10.6.5 Syntaxzusammenfassung FileStream System.IO.FileStream-Objekt erzeugen New IO.FileStream(filename, modus [,access [,sharing [,buffersize]]])
erzeugt ein FileStream-Objekt. modus (IO.FileMode-Aufzählung) gibt an, ob die Datei
neu erzeugt werden soll. access (IO.FileAccess-Aufzählung) gibt an, ob die Daten
gelesen oder geschrieben werden sollen. sharing (IO.FileShare-Aufzählung) gibt an, ob andere
Prozesse Zugriff auf die Daten haben.
10.6 Binärdateien lesen und schreiben
447
System.IO.FileStream-Klasse – Binärdateien lesen und schreiben Close()
schließt die Datei.
Flush()
speichert alle offenen Änderungen in der Datei.
Length
liefert die Länge der Datei.
Lock(pos, n)
blockiert n Bytes ab Position n für jeden Zugriff durch andere Prozesse.
Name
liefert den Namen der Datei.
Position
liefert die Position des Dateizeigers (0, wenn der Zeiger auf das erste Byte zeigt).
Read(b, offset, n)
liest n Bytes in das Byte-Feld b. Die Daten werden beginnend mit b(offset) in das Feld kopiert.
ReadByte()
liest ein Byte und liefert einen entsprechenden IntegerWert oder -1, wenn das Dateiende erreicht ist.
Seek(pos, start)
verändert die Position des Dateizeigers um pos Bytes. start (IO.SeekOrigin-Aufzählung) gibt die Startposition an.
Unlock(pos, n)
gibt n Bytes ab Position n für den Zugriff durch andere Prozesse wieder frei.
Write(b, offset, n)
speichert n Bytes, die aus dem Byte-Feld b beginnend mit b(offset) gelesen werden.
WriteByte(b)
speichert den Byte-Wert b.
System.IO.SeekOrigin-Aufzählung – Startposition für Seek() Begin
legt die Position relativ zum Dateianfang fest.
Current
ändert die Position relativ zur aktuellen Position.
End
legt die Position relativ zum Dateiende fest.
System.IO.FileMode-Aufzählung – Modus beim Öffnen von Dateien Append
öffnet die Datei, um an ihrem Ende Daten anzufügen. Es können keine Daten gelesen werden. Die Datei muss bereits existieren.
Create
erzeugt bzw. überschreibt die Datei.
CreateNew
erzeugt eine neue Datei. Die Datei darf noch nicht existieren.
Open
öffnet die angegebene Datei. Die Datei muss bereits existieren.
OpenOrCreate
öffnet die angegebene Datei oder erzeugt sie neu.
Truncate
öffnet die angegebene Datei und löscht ihren Inhalt. Die Datei muss bereits existieren.
448
10 Dateien und Verzeichnisse
System.IO.FileAccess-Aufzählung – Zugriffmodus auf Dateien Read
öffnet die Datei nur zum Lesen.
ReadWrite
öffnet die Datei zum Lesen und zum Schreiben.
Write
öffnet die Datei nur zum Schreiben.
System.IO.FileShare-Aufzählung – Sharing-Modus beim Dateizugriff None
verwehrt allen anderen Prozessen den Zugriff auf die Datei, bis diese geschlossen wird (Default bei Schreibzugriffen).
Read
erlaubt anderen Prozessen, die Datei gleichzeitig zu lesen (Default bei Lesezugriffen).
ReadWrite
erlaubt anderen Prozessen, die Datei gleichzeitig zu lesen und zu verändern.
Write
erlaubt anderen Prozessen, die Datei gleichzeitig zu verändern.
MemoryStream System.IO.MemoryStream-Klasse – Besondere Eigenschaften und Methoden Capacity
liefert die Größe des internen Pufferspeichers.
ToArray()
liefert den Inhalt des Streams als Byte-Feld.
WriteTo(stream)
speichert den Inhalt in einem anderen Stream-Objekt.
BinaryReader und -Writer System.IO.BinaryReader und System.IO.BinaryReader-Objekt erzeugen New IO.BinaryReader(filestream)
erzeugt aus einem FileStream-Objekt ein BinaryReaderObjekt.
New IO.StreamReader(filestream, enc) wie oben, aber unter Anwendung eines bestimmten Zeichensatzes (Text.Encoding). New IO.BinaryWriter(filestream)
erzeugt aus einem FileStream-Objekt ein BinaryWriterObjekt.
New IO.BinaryWriter(filestream, enc)
wie oben, aber unter Anwendung eines bestimmten Zeichensatzes (Text.Encoding).
10.7 Asynchroner Zugriff auf Dateien
449
System.IO.BinaryReader-Klasse – Binärdateien lesen Close()
schließt die Datei.
PeekChar()
liest ein Zeichen, ohne den Dateizeiger zu verändern.
Read()
liest ein Zeichen und liefert einen Integer-Wert oder -1, wenn das Dateiende erreicht ist.
ReadByte(), ReadChar() etc.
liest eine Variable des angegebenen Typs (soviele Bytes, wie zur Speicherung des Datentyps erforderlich sind). Falls das Dateiende erreicht wird, tritt ein EndOfStream-Fehler auf.
System.IO.BinaryWriter-Klasse – Binärdateien schreiben Close()
schließt die Datei.
Flush()
speichert alle offenen Änderungen auf dem Datenträger.
Seek(n, origin)
verändert die Position des Dateizeigers.
Write(x)
speichert die in x enthaltenen Daten in binärer Form. x muss einer der elementaren .NET-Datentypen sein (außer Boolean und Date).
10.7
Asynchroner Zugriff auf Dateien
Normalerweise erfolgt der Zugriff auf Dateien synchron. Das bedeutet, dass das Programm nach der Anweisung fs.Read(...) erst dann fortgesetzt wird, nachdem die angeforderten Daten gelesen wurden. Ebenso wird das Programm nach fs.Write(...) erst fortgesetzt, nachdem die Daten tatsächlich gespeichert wurden. (Aus Geschwindigkeitsgründen werden Lese- und Schreibzugriffe intern gepuffert, so dass nicht jeder Schreibvorgang auch sofort physikalisch auf der Festplatte ausgeführt wird.) Alle bisherigen Beispiele sind stillschweigend davon ausgegangen, dass der Datenzugriff synchron erfolgt. Ein asynchroner Zugriff bedeutet dagegen, dass Sie nach der Aufforderung, bestimmte Daten zu lesen oder zu schreiben, sofort mit anderen Kommandos weiterarbeiten können. Die asynchrone Dateioperation erfolgt in einem eigenen Thread, der zu diesem Zweck automatisch geschaffen wird. Nach Abschluss des Lese- oder Schreibvorgangs kann eine so genannte Callback-Prozedur zur Verständigung aufgerufen werden. Asynchrone Dateioperationen machen auf der einen Seite die Programmierung deutlich komplizierter, weil Sie sich nicht mehr darauf verlassen können, dass die gelesenen Daten sofort zur Verfügung stehen bzw. dass geschriebene Daten tatsächlich erfolgreich gespeichert werden konnten. Auf der anderen Seite bieten sie aber die Möglichkeit, Programme effizienter zu machen. Wenn Sie bei einem Programm DATEI|SPEICHERN asynchron implementieren, kann der Benutzer sofort weiterarbeiten, auch wenn das Speichern vielleicht
450
10 Dateien und Verzeichnisse
einige Sekunden dauert. Wenn Sie bei einem Programm Daten asynchron lesen, können Sie in der Zeit, bis die Daten verfügbar sind, andere Arbeiten erledigen.
Voraussetzungen •
Grundsätzlich sind asynchrone Dateioperationen nur bei großen Dateien sinnvoll und auch nur dann, wenn große Datenmengen auf einmal gelesen oder geschrieben werden. (Sind diese Voraussetzungen nicht erfüllt, sind asynchrone Operationen wegen des größeren Verwaltungsaufwands oft wesentlich langsamer als synchrone!)
•
Je nach Betriebssystem werden asynchrone Dateioperationen womöglich gar nicht unterstützt. Laut Dokumentation wird New auch dann fehlerfrei ausgeführt, die unten beschriebenen Methoden BeginWrite bzw. BeginRead werden aber dennoch synchron ausgeführt. Ich habe asynchrone Dateioperationen unter Windows 2000 getestet, und dort haben sie gut funktioniert.
•
Asynchrone Operationen sind nur für binäre Datenströme vorgesehen (Klassen Stream, FileStream etc.). Die Methoden BeginWrite und BeginRead können nur Byte-Felder verarbeiten.
10.7.1 Programmiertechniken Asynchrones FileStream-Objekt erzeugen Bevor asynchrone Operationen überhaupt möglich sind, muss ein FileStream-Objekt im asynchronen Modus erzeugt werden. Der dazu vorgesehene New-Konstruktor verlangt ziemlich viele Parameter: Dim fs As New IO.FileStream(filename, createmode, accessmode, _ sharemode, buffersize, True)
Die drei Parameter createmode, accessmode und sharemode wurden bereits in Abschnitt 10.6.1 beschrieben. buffersize gibt die Größe des Puffers an, der beim Lesen bzw. Schreiben verwendet werden soll. Dieser Puffer muss mindestens 64 kByte betragen, andernfalls werden die Dateioperationen im synchronen Modus ausgeführt.
Daten asynchron schreiben und lesen Um nun Daten asynchron in der Datei zu speichern, verwenden Sie statt Write die Methode BeginWrite. Die zu schreibenden Daten müssen als eindimensionales Bytefeld übergeben werden. Die Parameter start und n geben an, ab welcher Position innerhalb des Felds und wie viele Bytes gespeichert werden sollen. Die beiden weiteren Parameter können die Adresse einer Callback-Prozedur und ein Objekt zur Identifizierung des Schreibvorgangs enthalten (siehe unten).
10.7 Asynchroner Zugriff auf Dateien
451
BeginWrite wird im Gegensatz zu synchronen Schreibmethoden sofort beendet. Als Ergebnis erhalten Sie ein Objekt, das die Schnittstelle IAsyncResult realisiert. Mit dessen Eigenschaft IsCompleted können Sie ganz einfach überprüfen, ob der Schreibvorgang bereits abgeschlossen ist. Darüber hinaus liefert die Eigenschaft AsyncWaitHandle ein Threading.WaitHandle-Objekt, dessen Methoden bei Bedarf eine Synchronisierung des Lese- oder
Schreib-Threads ermöglichen. Während das Programm nun im Hintergrund die Daten in die Datei überträgt, können Sie in Ihrem Programm andere Dinge erledigen. (Falls dabei Fehler auftreten können, müssen Sie eine Fehlerabsicherung durchführen, damit der Schreibvorgang sicher zu Ende geführt werden kann!) Um den Programmfluss wieder zu synchronisieren, führen Sie EndWrite aus, wobei Sie das IAsyncResult-Objekt übergeben. EndWrite wartet, bis der Schreibvorgang tatsächlich zu Ende ist. Dim result As IAsyncResult result = fs.BeginWrite(byte_array, start, n, Nothing, Nothing) ... andere Dinge erledigen fs.EndWrite(result) 'auf das Ende des Schreibvorgangs warten
Analog zu Begin-/EndWrite gibt es auch die Methoden BeginRead und EndRead. Damit können Sie Daten asynchron in ein Byte-Feld einlesen. Beachten Sie, dass Sie auf das Feld mit den Ergebnisdaten erst nach EndRead zugreifen können! Wenn Sie die Daten sofort benötigen, sollten Sie eine synchrone Schreibmethode verwenden.
Asynchrone Callbacks Statt mit EndRead/-Write auf das Ende der asynchronen Operation zu warten, können Sie sich davon auch durch eine so genannte Callback-Prozedur verständigen lassen. Eine Callback-Prozedur ist mit einer Ereignisprozedur vergleichbar, die automatisch aufgerufen wird. Diese Prozedur muss folgendermaßen deklariert werden (wichtig ist der Parameter des Typs IAsyncResult). ' wird zum Ende der asynchronen Operation aufgerufen Public Sub my_callback_func(ByVal ar As IAsyncResult) ... End Sub
Beim Aufruf von BeginWrite oder -Read übergeben Sie nun im vierten Parameter die Adresse dieser Prozedur. Im fünften Parameter können Sie ein beliebiges Objekt data angeben, das dann an die Callback-Prozedur weitergegeben wird. Dieses Objekt übernimmt zwei Aufgaben: es kann innerhalb der Callback-Prozedur helfen, die Quelle des Aufrufs zu identifizieren (falls Sie die Callback-Prozedur für verschiedene asynchrone Operationen in Ihrem Programm verwenden), und es kann dazu verwendet werden, um bestimmte Arbeiten zu erledigen, mit denen Sie warten müssen, bis die asynchrone Operation zu Ende ist.
452
10 Dateien und Verzeichnisse
' fs ist ein IO.FileStream-Objekt, siehe oben result = fs.BeginWrite(byte_array, start, n, _ AddressOf my_callback_func, data)
Innerhalb der Callback-Funktion können Sie über ar auf das oben schon beschriebene Objekt der Schnittstelle IAsyncResult zugreifen. Neu ist, dass Sie nun über die Eigenschaft ar.AsyncState auf das Objekt zugreifen können, dass Sie im fünften Parameter von BeginWrite oder -Read übergeben haben.
Fehlerabsicherung Wenn bereits als Reaktion auf die Methode BeginRead oder -Write ein Fehler auftritt, können Sie davon ausgehen, dass die asynchrone Operation gar nicht gestartet wurde. (In diesem Fall kommt es daher auch nicht zum Aufruf von Callback-Prozeduren.)
VERWEIS
Komplizierter wird es, wenn während der asynchronen Operation ein Fehler auftritt. In diesem Fall wird die entsprechende Exception erst ausgelöst, wenn Sie EndRead oder -Write ausführen. Allgemeine Grundlageninformationen zur asynchronen Programmierung (also losgelöst von den hier beschriebenen IO-Anwendungen) finden Sie in der OnlineHilfe, wenn Sie nach Einschließen asynchroner Aufrufe suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpconasynchronousprogramming.htm
10.7.2 Beispiel – Vergleich synchron/asynchron Das folgende Beispielprogramm wird durch die beiden Prozeduren write_file_async und write_file_sync dominiert. write_file_async schreibt ein Byte-Feld asynchron in eine Datei, wobei ein Buffer von 256 kByte verwendet wird. Während des asynchronen Schreibvorgangs werden in der Prozedur do_some_work eine Menge Sinuswerte berechnet. write_file_sync erfüllt dieselbe Aufgabe, schreibt die Datei aber synchron und muss daher zuerst auf das Ende des Schreibvorgangs warten, bevor es do_some_work ausführen kann. Um die beiden Methoden zu vergleichen, werden write_file_[a]sync je fünf Mal ausgeführt und die Laufzeiten im Konsolenfenster angezeigt. (Achtung, dabei werden vorübergehend zwei temporäre Dateien mit einem Gesamtspeicherbedarf von 500 MByte erstellt!) Abbildung 10.13 beweist zweierlei: Einerseits, dass die asynchrone Vorgehensweise in diesem Fall wirklich deutlich effizienter ist, andererseits, dass die Zeiten relativ stark variieren können (abhängig davon, wie rasch es dem Betriebssystem gelingt, die Dateien auf die Festplatte zu schreiben). Es kann sogar vorkommen, dass eine asynchrone Operation länger dauert als eine synchrone, im Durchschnitt ist die asynchrone Variante aber deutlich effizienter.
10.7 Asynchroner Zugriff auf Dateien
Abbildung 10.13: Messzeiten für das synchrone und asynchrone Schreiben von Dateien
' Beispiel dateien\async-test Module Module1 Const nr_of_bytes As Integer = 1024 * 1024 * 50 ' 50 MByte Const buffer_size As Integer = 1024 * 256 '256 kByte Sub main() Dim i As Integer ' Byte-Feld mit Zufallsdaten füllen Dim b(nr_of_bytes) As Byte Dim rndm As New System.Random() rndm.NextBytes(b) ' zwei temporäre Dateien erzeugen Dim fname1 As String = IO.Path.GetTempFileName() Dim fname2 As String = IO.Path.GetTempFileName() ' Byte-Feld speichern For i = 1 To 5 write_file_async(fname1, b) Next For i = 1 To 5 write_file_sync(fname2, b) Next ' Dateien löschen IO.File.Delete(fname1) IO.File.Delete(fname2) End Sub
453
454
10 Dateien und Verzeichnisse
Sub write_file_async(ByVal fname As String, ByVal b_array As Byte()) Dim starttime As Date = Now Dim result As IAsyncResult Dim fs As IO.FileStream fs = New IO.FileStream(fname, IO.FileMode.Append, _ IO.FileAccess.Write, IO.FileShare.None, buffer_size, True) result = fs.BeginWrite(b_array, 0, b_array.Length, _ Nothing, Nothing) do_some_work() fs.EndWrite(result) 'auf Ende der asynchronen Operation warten fs.Close() Console.WriteLine("async: " + Now.Subtract(starttime).ToString) End Sub Sub write_file_sync(ByVal fname As String, ByVal b_array As Byte()) Dim starttime As Date = Now Dim fs As IO.FileStream fs = New IO.FileStream(fname, IO.FileMode.Append) fs.Write(b_array, 0, b_array.Length) do_some_work() fs.Close() Console.WriteLine("sync: " + Now.Subtract(starttime).ToString) End Sub ' sinnlose Berechnungen durchführen ... Sub do_some_work() Dim i As Integer Dim x As Double For i = 0 To nr_of_bytes \ 5 x += Math.Sin(i) Next End Sub End Module
10.7.3 Beispiel – Asynchroner Callback-Aufruf Das zweite Beispiel ist eine Variation des ersten. Abermals wird eine große Datei asynchron gespeichert und währenddessen eine Berechnung durchgeführt. Neu ist, dass zum Ende des Speichervorgangs die Prozedur async_callback_func aufgerufen wird. Abbildung 10.14 veranschaulicht die Gleichzeitigkeit der beiden Operation.
10.7 Asynchroner Zugriff auf Dateien
455
Abbildung 10.14: Während der Berechnung erscheint die Meldung, dass der asynchrone Schreibvorgang beendet ist
Auf einen Abdruck des gesamten Beispielprogramms wird diesmal aus Platzgründen verzichtet; die meisten Details sehen ohnedies ganz ähnlich aus wie im vorigen Beispiel. Entscheidend ist der Aufruf von BeginWrite, wobei als Parameter die Adresse von async_callback_func sowie das FileStream-Objekt übergeben werden. Das ermöglicht es, in der Callback-Prozedur die Dateigröße anzuzeigen. ' Beispiel dateien\async-callback Sub main() ... fs = New IO.FileStream(fname, IO.FileMode.Append, _ IO.FileAccess.Write, IO.FileShare.None, buffer_size, True) result = fs.BeginWrite(b_array, 0, b_array.Length, _ AddressOf async_callback_func, fs) ... End Sub ' Callback-Prozedur Public Sub async_callback_func(ByVal ar As IAsyncResult) Dim fs As IO.FileStream = CType(ar.AsyncState, IO.FileStream) Console.WriteLine("asynchroner Schreibvorgang beendet: {0}", _ ar.IsCompleted) Console.WriteLine("Dateigröße in Bytes: {0}", fs.Length) End Sub
456
10 Dateien und Verzeichnisse
10.8
Verzeichnis überwachen
HINWEIS
Die Klasse io.FileSystemWatcher ermöglicht es, ein Verzeichnis des Dateisystems zu überwachen. Jedes Mal, wenn sich in diesem Verzeichnis eine Datei ändert, wird ein Ereignis ausgelöst. Die Klasse ist im Gegensatz zu den anderen IO-Klassen in der Bibliothek System.dll definiert. Diese Bibliothek steht wie mscorlib.dll allen VB.NET-Programmen automatisch zur Verfügung. Die FileSystemWatcher-Klasse kann nur unter Windows NT/2000/XP genutzt werden! An die Ereignisprozeduren werden aber trotz dieser Betriebssystemanforderung Dateinamen in der DOS-kompatiblen 8+3-Schreibweise übergeben. Warum das so ist, weiß allein Microsoft ...
Zur Anwendung der FileSystemWatcher-Klasse definieren Sie mit WithEvents auf Moduloder Klassenebene eine Variable des Typs FileSystemWatcher. Anschließend erzeugen Sie ein neues Objekt dieser Klasse, wobei Sie an den New-Konstruktor den Namen des Verzeichnisses übergeben, das Sie überwachen möchten. Durch EnableRaisingEvents=True starten Sie den Ereignisfluss. Es werden nun in Ihrem Programm enthaltene Ereignisprozeduren für die FileSystemWatcher-Variable aufgerufen, wobei aus den Eigenschaften des Parameters e unter anderem der Name der geänderten Datei ermittelt werden kann. Die FileSystemWatcher-Klasse sieht fünf Ereignisse vor: Created, Change, Renamed, Deleted und Error. Die ersten vier Ereignisse beziehen sich auf die Dateien im überwachten Verzeichnis. Das Error-Ereignis tritt auf, wenn der interne Puffer zur Verwaltung der Dateiereignisse überschritten wird (siehe Überschrift Pufferüberlauf). Module Module1 Dim WithEvents fsw As IO.FileSystemWatcher Sub Main() fsw = New IO.FileSystemWatcher(IO.Path.GetTempPath()) fsw.EnableRaisingEvents = True Console.WriteLine("Return beendet das Programm") Console.ReadLine() End Sub
VERWEIS
Public Sub fsw_Changed(ByVal sender As Object, _ ByVal e As System.IO.FileSystemEventArgs) Handles fsw.Changed Console.WriteLine("Datei {0} hat sich geändert", e.FullPath) End Sub End Module
Ein vollständiges Anwendungsbeispiel für die FileSystemWatcher-Klasse finden Sie im Beispielprogramm oo-programmierung\intro-event, das in Abschnitt 6.1.3 beschrieben wird.
10.8 Verzeichnis überwachen
457
Überwachungsbereich modifizieren Per Default überwacht FileSystemWatcher alle Dateien, die sich direkt im angegebenen Verzeichnis befinden. Wenn Sie auch alle Unterverzeichnisse überwachen möchten, müssen Sie IncludeSubdirectories auf True setzen. Wenn Sie nur bestimmte Dateien (z.B. "*.gif") überwachen möchten, können Sie die FilterEigenschaft mit einem entsprechenden Muster einstellen. Die Defaulteinstellung lautet "*.*". Schließlich können Sie mit NotifyFilters angeben, welcher Art die Veränderungen sein sollen, die Sie beobachten möchten. Per Default werden Änderungen am Namen und am Inhalt beobachtet. Sie können aber beispielsweise auch Veränderungen an den Sicherheitseinstellungen oder an den Dateiattributen verfolgen. Die Einstellung der Eigenschaft erfolgt durch eine Or-Kombination von Konstanten aus der NotifyFilters-Aufzählung.
Pufferüberlauf Wenn mit FileSystemWatcher eine Überwachung eingerichtet wird, wird jede Änderung am Dateisystem in einem Puffer zwischengespeichert. Die einzelnen Einträge des Puffers werden dann der Reihe nach durch die Aufrufe von Ereignisprozeduren abgearbeitet. Wenn plötzlich sehr viele Dateien erzeugt, geändert oder gelöscht werden und die FileSystemWatcher-Ereignisprozeduren nicht schnell genug verarbeitet werden können, reicht die Größe des Zwischenpuffers nicht aus. In diesem Fall tritt das Error-Ereignis auf, an das ein Objekt der Klasse InternalBufferOverflowException übergeben wird. (Das Objekt kann mit e.GetException() ausgewertet werden.) Durch diesen Fehler wird der Puffer gelöscht und die Überwachung beendet. Um neuerlich Ereignisse empfangen zu können, muss ein neues FileSystemWatcher-Objekt erzeugt werden. Dabei ist es sinnvoll, mit InternalBufferSize einen größeren Puffer zu wählen. Die Angabe des Puffers erfolgt in Byte und sollte ein Vielfaches von 4096 betragen. Die Defaultgröße beträgt 8192 Byte. Die folgende Prozedur zeigt eine mögliche Reaktion auf den Fehler.
TIPP
' Beispiel oo-programmierung\intro-event Public Sub fsw_Error(ByVal sender As Object, _ ByVal e As System.IO.ErrorEventArgs) Handles fsw.Error Console.WriteLine(e.GetException.GetType.FullName + vbCrLf + _ e.GetException.Message) fsw = New IO.FileSystemWatcher(IO.Path.GetTempPath()) fsw.InternalBufferSize = 4096 * 16 fsw.EnableRaisingEvents = True End Sub
Achten Sie darauf, dass Sie nicht mehr überwachen, als unbedingt notwendig ist. Nutzen Sie die Eigenschaften Filter, NotifyFilter und IncludeSubdirectories, um den Geltungsbereich des FileSystemWatcher-Objekts einzuschränken.
458
10 Dateien und Verzeichnisse
Syntaxzusammenfassung System.IO.FileSystemWatcher – Eigenschaften EnableRaisingEvents
aktiviert den Aufruf der Ereignisprozeduren (True/False).
Filter
gibt ein Muster für die zu überwachenden Dateinamen an (per Default "*.*")
IncludeSubdirectories
gibt an, ob auch Unterverzeichnisse überwacht werden sollen (per Default False).
InternalBufferSize
bestimmt die Größe des Zwischenspeichers in Byte.
NotifyFilter
gibt an, welcher Art die Änderungen an der Datei sein sollen (z.B. NotifyFilters.Size).
System.IO.FileSystemWatcher – Ereignisse Changed
gibt an, dass der Inhalt einer Datei geändert wurde.
Created
gibt an, dass eine neue Datei erzeugt wurde.
Deleted
gibt an, dass eine Datei gelöscht wurde.
Error
es ist ein Fehler bei der Verwaltung des FileSystemWatcherZwischenspeichers aufgetreten.
Renamed
gibt an, dass eine Datei umbenannt wurde.
10.9
Serialisierung
Der schwer zu übersetzende Fachausdruck serialization bezeichnet die Umwandlung eines Objekts in eine Binär- oder Textform (XML), die anschließend über ein Netzwerk an einen anderen Rechner übertragen oder in einer Datei gespeichert werden kann. Die Rückverwandlung in Objekte wird Deserialisierung (englisch de-serialization) genannt. Der Begriff Serialisierung bezieht sich darauf, dass die im allgemeinen Fall hierarchische Verknüpfung verschiedener Objekte in eine serielle Form, also gewissermaßen in eine flache Darstellung umgewandelt werden muss. Für die Serialisierung und Deserialisierung gibt es eine ganze Reihe von Anwendungen: •
Übertragung von Objekten via Netzwerk: damit können Prozesse auf unterschiedlichen Rechnern Objekte austauschen.
•
Speicherung von Objekten in einer Datei: damit können Objekte persistent gespeichert und zu einem späteren Zeitpunkt wieder geladen werden. (Diese Form der Anwendung ist auch der Grund, weswegen Serialisierung in diesem Kapitel beschrieben wird.)
•
Speicherung von Objekten in einer Datenbank.
10.9 Serialisierung
459
VERWEIS
Dieser Abschnitt gibt nur eine Einführung in das Thema Serialisierung. Eine ausführlichere Beschreibung finden Sie in der Hilfe, wenn Sie nach Serialisieren von Objekten suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpovrserializingobjects.htm
Wenn Sie sich speziell für den Datenaustausch zwischen Prozessen auf unterschiedlichen Rechnern interessieren, müssen Sie sich auch in das Thema .NET Remoting einlesen: ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpconnetremotingoverview.htm
10.9.1 Grundlagen Serialisierungsformate Im Zusammenhang mit Serialisierung bezeichnet der Begriff Formatierung die Umwandlung von Objekten zu einem Byte-Strom. Die folgende Tabelle zählt die drei in der .NETBibliothek vorgesehenen Formatierungsklassen auf. Damit können Sie Objekte in einem Binärformat, in einem XML-Format oder in einem SOAP-Format (de-)serialisieren. Formatierungsklassen zum (De-)Serialisieren von Objekten Runtime.Serialization.Formatters.Binary.BinaryFormatter
Bibliothek mscorlib.dll
Xml.Serialization.XmlSerializer
Bibliothek System.Xml.dll
Runtime.Serialization.Formatters.Soap.SoapFormatter
Bibliothek System.Runtime.Serialization.Formatters.Soap.dll
XML steht für Extensible Markup Language und bezeichnet ein Textformat zur Darstellung beinahe beliebiger (auch hierarchischer) Daten. SOAP steht für Simple Object Access Protocol und beschreibt ein standardisiertes Format zum Austausch von Objekten zwischen verschiedenen Prozessen. SOAP basiert auf XML, d.h., die Objekte werden in Form von XMLDateien oder -Datenströmen weitergegeben. Natürlich stellt sich jetzt sofort die Frage, welches Format Sie einsetzen sollen. Die folgenden Punkte sollen bei der Beantwortung dieser Frage helfen. •
Wenn es Ihnen darum geht, die Daten möglichst kompakt und effizient zu serialisieren, ist BinaryFormatter die beste Wahl. Allerdings ist dieses Format nur innerhalb der .NETWelt verständlich.
•
Wenn Sie dagegen Wert auf offene Standards legen und Daten mit Programmen austauschen möchten, die selbst nicht auf den .NET-Bibliotheken basieren, sind SoapFormatter und XmlSerializer besser geeignet. Bei beiden Varianten sind die resultierenden Datenströme aber wesentlich größer als beim Binärformat, die Verarbeitung dauert entsprechend länger.
460
•
10 Dateien und Verzeichnisse
BinaryFormatter ist am systemnächsten. Laut Online-Hilfe wird bei der (De-)Serialisierung die Typintegrität beibehalten (type fidelity im englischen Orginal). Was das bedeutet,
beschreibt die Online-Hilfe leider nicht. Praktische Tests ergaben, dass sich mit dem BinaryFormatter manche .NET-Objekte serialisieren und wieder deserialisieren lassen, bei denen SoapFormatter und XmlSerializer nur Fehlermeldungen liefern (z.B. ein Drawing.Font-Objekt). •
BinaryFormatter und SoapFormatter kommen mit Rekursion zurecht. Der XmlSerializer
kann dagegen nur dann miteinander verknüpfte Objekte (de-)serialisieren, solange es dabei keine zirkuläre Verweise gibt. •
BinaryFormatter und SoapFormatter setzen voraus, dass selbst definierte Klassen mit dem Attribut ausgestattet sind, damit davon abgeleitete Objekte serialisiert werden können. XmlSerializer funktioniert auch ohne diese Voraussetzung, dafür muss die Klasse aber explizit als Public deklariert werden. (Wenn Sie das vergessen, liefert der New-Konstruktor von XmlSerializer nur eine irreführende Fehlermeldung.)
•
HINWEIS
Die Anwendung und Steuerung von BinaryFormatter und SoapFormatter erfolgt weitgehend identisch. Daher ist es meist problemlos möglich, zwischen diesen beiden Formaten zu wechseln. Dagegen stellt XmlSerializer eine vollkommen eigenständige und in vielen Details inkompatible Implementierung dar. Dieser Abschnitt bezieht sich überwiegend auf BinaryFormatter und SoapFormatter. XmlSerializer wird nur am Rande behandelt. Beachten Sie, dass Sie vor der Verwendung des SoapFormatters einen Verweis auf die Bibliothek System.Runtime.Serialization.Formatters.Soap.dll einrichten müssen!
Objekt serialisieren Um ein Objekt zu serialisieren, benötigen Sie ein Formatter-Objekt. Anschließend übergeben Sie an dessen Serialize-Methode ein IO.Stream-Objekt einer Datei oder einer Netzwerkverbindung sowie das zu serialisierende Objekt: Dim binform As New _ Runtime.Serialization.Formatters.Binary.BinaryFormatter() binform.Serialize(streamobj, dataobj)
Selbstverständlich können Sie nun mit binform auch mehrere Objekte hintereinander serialisieren. (Sie müssen aber bei der Deserialisierung darauf achten, dass Sie die Objekte in der gleichen Reihenfolge wieder auslesen.) Statt der BinaryFormatter-Klasse können Sie ebenso gut eine SoapFormatter-Klasse verwenden. Auch der Einsatz der XmlSerializer-Klasse ändert nicht viel an der Vorgehensweise, allerdings muss als New-Parameter der Klassentyp des zu speichernden Stammobjekts
10.9 Serialisierung
461
übergeben werden. (dataobj darf aber durchaus auf Objekte anderer Klassen verweisen, die ebenfalls serialisiert werden.) Dim xmlform As New Xml.Serialization.XmlSerializer(dataobj.GetType()) xmlform.Serialize(streamobj, dataobj)
Objekt deserialisieren Bevor Sie ein Objekt deserialisieren können, benötigen Sie abermals ein Objekt der Klassen BinaryFormatter, SoapFormatter oder XmlSerializer, das wie vorhin erzeugt wird. Darauf wenden Sie die Methode Deserialize an. Diese Methode liefert die Daten als Object zurück, weswegen Sie mit CType eine Konvertierung durchführen müssen: dataobj = CType(binform.Deserialize(streamobj), dataclass)
HINWEIS
Eigene Klassen (de-)serialisieren Alle weiteren Informationen in diesem Abschnitt beziehen sich explizit nur auf Binary- und SoapFormatter! Die Serialisierung eigener Klassen mit XmlSerializer wird durch andere Attribute gesteuert, die hier aber nicht beschrieben werden.
Damit ein Objekt einer selbst definierten Klasse (de-)serialisiert werden kann, muss die ganze Klasse mit dem Attribut gekennzeichnet werden. .NET speichert nun bei der Serialisierung automatisch alle Klassenvariablen (unabhängig davon, ob diese Public oder Private sind). Bei der Serialisierung werden die Klassenvariablen direkt ausgelesen, d.h., es werden dazu keine Eigenschaften oder Methoden eingesetzt. Bei der Deserialisierung wird ein neues Objekt erzeugt, wobei alle Klassenvariabeln rekonstruiert werden. Dabei wird weder einer der New-Konstruktoren ausgeführt, noch werden irgendwelche Eigenschaften oder Methode der Klasse benutzt. Wenn Sie möchten, dass einzelne Klassenvariablen nicht serialisiert werden, können Sie diese als kennzeichnen. Class class1 Public a As Integer Public b As String ... End Class
ISerializable-Schnittstelle Wenn Ihnen die automatische (De-)Serialisierung zu wenig Flexibilität bietet, können Sie die Sache auch selbst in die Hand nehmen. Dazu müssen Sie in Ihrer Klasse die Runtime.Serialization.ISerializable-Schnittstelle implementieren. Diese Schnittstelle muss zusätzlich zum Attribut angegeben werden.
462
10 Dateien und Verzeichnisse
Die ISerializable-Schnittstelle bewirkt, dass nun automatisch gar keine Daten mehr (de-)serialisiert werden. Stattdessen müssen Sie sich selbst um die Serialisierung in der Methode GetObjectData und um die Deserialisierung in einem zusätzlichen New-Konstruktor kümmern. Die folgenden Zeilen geben ein Codegerüst an: Class class1 Implements Runtime.Serialization.ISerializable ' Deserialisierung Public Sub New( _ ByVal info As Runtime.Serialization.SerializationInfo, _ ByVal context As Runtime.Serialization.StreamingContext) a = info.GetInt32("a") b = info.GetString("b") ... End Sub ' Serialisierung Public Overridable Overloads Sub GetObjectData( _ ByVal info As Runtime.Serialization.SerializationInfo, _ ByVal context As Runtime.Serialization.StreamingContext) _ Implements Runtime.Serialization.ISerializable.GetObjectData info.AddValue("a", a) info.AddValue("b", b) ... End Sub End Class
An GetObjectData wird ein Objekt der Klasse SerializationInfo-Objekt übergeben. Mit dessen Methode AddValue übergeben Sie alle zu serialisierenden Daten (meist Klassenvariablen). Dabei muss jede Variable durch eine eindeutige Zeichenkette gekennzeichnet werden. Im zusätzlichen New-Konstruktor erfolgt der umgekehrte Vorgang: Mit diversen GetXxxMethoden können Sie die Daten aus dem SerializationInfo-Objekt wieder extrahieren. Die Get-Methoden stehen für alle elementaren Datentypen zur Verfügung, z.B. GetString, GetSingle etc. Bei Integerzahlen gilt: GetInt16 für Short, GetInt32 für Integer, GetInt64 für Long. Um Objekte einer beliebigen Klasse zu extrahieren, müssen Sie die Methode GetValue verwenden. An diese Methode müssen Sie im zweiten Parameter den zu erwartenden Klassentyp übergeben. Außerdem müssen Sie die resultierenden Daten, die von GetValue als Object zurückgegeben werden, mit CType in ein Objekt der entsprechenden Klasse umwandeln. c = CType(info.GetValue("c", GetType(klassenname)), klassenname)
10.9 Serialisierung
463
10.9.2 Beispiel – String-Feld serialisieren Das folgende Beispielprogramm erzeugt ein String-Feld zufälliger Größe und Inhalts. Dieses Feld wird in eine temporäre Datei serialisiert und dann von dort wieder neu eingelesen. Zur Kontrolle wird der Inhalt des Felds vorher und nachher angezeigt. Außerdem wird die Serialialisierungsdatei im Konsolenfenster angezeigt (siehe Abbildung 10.15).
Abbildung 10.15: Beispielprogramm zur Serialisierung eines String-Felds
Das Programm erzeugt in der Prozedur create_array ein zufälliges String-Feld und zeigt dieses mit show_array am Bildschirm an. Das Feld wird dann mit save_array_xxx in einem von drei Formaten gespeichert und anschließend mit load_array_soap_xxx wieder eingelesen. ' Beispiel dateien\serialize-array Sub Main() Dim s() As String Dim fname As String = IO.Path.GetTempFileName() Dim fs As IO.FileStream ' Feld mit Zufallsdaten erzeugen und anzeigen create_array(s) show_array(s) ' Array speichern Console.WriteLine("Temporäre Datei: {0}", fname) fs = New IO.FileStream(fname, IO.FileMode.Open)
464
10 Dateien und Verzeichnisse
save_array_soap(fs, s) fs.Close() ' Array löschen und neu laden Erase s fs = New IO.FileStream(fname, IO.FileMode.Open) load_array_soap(fs, s) fs.Close() show_array(s) ' Serialisierungsdatei anzeigen Dim sr As New IO.StreamReader(fname) Console.WriteLine("Der Inhalt der Serialisierungsdatei:") Console.WriteLine(sr.ReadToEnd()) sr.Close() IO.File.Delete(fname) End Sub Sub save_array_binary(ByVal fs As IO.FileStream, ByVal s As String()) Dim binform As New _ Runtime.Serialization.Formatters.Binary.BinaryFormatter() binform.Serialize(fs, s) End Sub Sub save_array_soap(ByVal fs As IO.FileStream, ByVal s As String()) Dim soapform As New _ Runtime.Serialization.Formatters.Soap.SoapFormatter() soapform.Serialize(fs, s) End Sub Sub save_array_xml(ByVal fs As IO.FileStream, ByVal s As String()) Dim xmlform As New Xml.Serialization.XmlSerializer(s.GetType()) xmlform.Serialize(fs, s) End Sub Sub load_array_binary(ByVal fs As IO.FileStream, ByRef s As String()) Dim binform As New _ Runtime.Serialization.Formatters.Binary.BinaryFormatter() s = CType(binform.Deserialize(fs), String()) End Sub Sub load_array_soap(ByVal fs As IO.FileStream, ByRef s As String()) ' analog zu load_array_binary(...) ... End Sub Sub load_array_xml(ByVal fs As IO.FileStream, ByRef s As String()) Dim xmlform As New _ Xml.Serialization.XmlSerializer(GetType(String())) s = CType(xmlform.Deserialize(fs), String()) End Sub
10.9 Serialisierung
465
10.9.3 Beispiel – Objekte eigener Klassen serialisieren Im Beispielprogramm werden zwei Objekte data1 und data2 erzeugt, in einen MemoryStream serialisiert und von dort in get1 und get2 deserialisiert. Anschließend zeigt das Beispielprogramm die Klassenvariablen der vier Variablen an (siehe Abbildung 10.2).
Abbildung 10.16: Beispielprogramm zur (De-)Serialisierung eigener Klassen
Die Klasse class1 besteht aus vier Klassenvariablen a, b, c und d für verschiedene Datentypen. c enthält ein Objekt der Klasse System.Random, um so auch die Serialisierung nicht ganz trivialer Daten zu demonstieren. Die New-Konstruktoren dienen zur Initialisierung, WriteToConsole schreibt den Inhalt von a bis d in das Konsolenfenster. Die gesamte Klasse ist mit dem Attribut Serializable gekennzeichnet. ' Beispiel dateien\serialize-test Class class1 Public a As Integer Public b As String Public c As Random Public d As Date Public Sub New() Me.New(0, "", New Random()) End Sub Public Sub New(ByVal a As Integer, ByVal b As String, _ ByVal c As Random) Me.a = a : Me.b = b : Me.c = c : d = Now End Sub Public Sub WriteToConsole(ByVal name As String) Console.Write(name + ": ") Console.WriteLine( _ "a={0}, b={1}, Zufallszahl c.Next()={2}, d={3}", _ a, b, c.Next(), d) End Sub End Class class2 ist von class1 vererbt und realisiert außerdem die Schnittstelle ISerializable. Die beiden
elementaren Konstruktoren rufen einfach den Konstruktor der Basisklasse auf. Neu ist der dritte Konstruktor, der wegen der ISerializable-Schnittstelle bei der Deserialisierung aufge-
466
10 Dateien und Verzeichnisse
VERWEIS
rufen wird. In dieser Prozedur werden die Klassenvariablen a bis d initialisiert. a bis c werden aus dem SerializationInfo-Objekt extrahiert. d wird dagegen mit der aktuellen Zeit initialisiert (um so zu zeigen, dass Sie durch die ISerializable-Schnittstelle Flexibilität bei der Initialisierung gewinnen). Damit gibt class2.d den Zeitpunkt an, zu dem das Objekt deserialisiert wurde, während class1.d den Zeitpunkt angibt, zu dem das Objekt zum ersten Mal erzeugt wurde. Für die Serialisierung ist die Methode GetObjektData verantwortlich. Dort werden a bis d in das SerializationInfo-Objekt übertragen. Ein weiteres Beispiel für die Realisierung der ISerializable-Schnittstelle finden Sie in Abschnitt 11.1.1, wo die Programmierung einer eigenen Exception-Klasse erklärt wird.
'Klasse mit ISerializable-Schnittstelle Class class2 Inherits class1 Implements Runtime.Serialization.ISerializable Public Sub New() MyBase.New() End Sub Public Sub New(ByVal a As Integer, ByVal b As String, _ ByVal c As Random) MyBase.New(a, b, c) End Sub ' Deserialisierung Public Sub New( _ ByVal info As System.Runtime.Serialization.SerializationInfo, _ ByVal context As System.Runtime.Serialization.StreamingContext) a b c d End
= info.GetInt32("a") = info.GetString("b") = CType(info.GetValue("c", GetType(Random)), Random) = Now Sub
' Serialisierung Public Overridable Overloads Sub GetObjectData( _ ByVal info As System.Runtime.Serialization.SerializationInfo, _ ByVal context As System.Runtime.Serialization.StreamingContext) _ Implements Runtime.Serialization.ISerializable.GetObjectData info.AddValue("a", a) info.AddValue("b", b) info.AddValue("c", c) End Sub End Class
10.9 Serialisierung
467
Bei der Anwendung der Klassen gibt es wenig Besonderheiten. Die Serialisierung erfolgt in einen MemoryStream, um das Erzeugen einer temporären Datei zu vermeiden. Vor der Deserialisierung legt das Programm eine Pause von drei Sekunden ein, um die unterschiedliche Behandlung der Klassenvariable d in class1 und class2 zu demonstrieren. ' Beispiel dateien\serialize-test Sub Main() Dim i As Integer Dim ms As New IO.MemoryStream() Dim frmt As New _ Runtime.Serialization.Formatters.Binary.BinaryFormatter() ' Objekte erzeugen, serialisieren und ihren Inhalt anzeigen Dim data1 As New class1(123, "abc", New Random()) Dim data2 As New class2(456, "xxx", New Random()) frmt.Serialize(ms, data1) frmt.Serialize(ms, data2) Console.WriteLine("3 Sekunden Pause ...") Threading.Thread.Sleep(3000) ' Objekte deserialisieren Dim get1 As class1, get2 As class2 ms.Seek(0, IO.SeekOrigin.Begin) get1 = CType(frmt.Deserialize(ms), class1) get2 = CType(frmt.Deserialize(ms), class2) ' Quelldaten und Ergebnisse anzeigen Console.WriteLine("Ausgangsdaten:") data1.WriteToConsole("data1") data2.WriteToConsole("data2") Console.WriteLine("Deserialisierte Daten:") get1.WriteToConsole("get1") get2.WriteToConsole("get2") End Sub
10.9.4 Beispiel – LinkedList serialisieren In Kapitel 7 wurde zur Erläuterung des objektorientierten Programmierens die Klasse LinkedList vorgestellt. Das folgende Beispiel beweist, dass sich eine Kette von LinkedListObjekten problemlos mit Binary- oder SoapFormatter serialisieren lässt. (XmlSerializer ist dazu ungeeignet, weil die Vor- und Rückverweise der LinkedList-Objekte zirkulär sind.) Die einzige Änderung, die dazu an der LinkedList-Klasse erforderlich ist, ist das Attribut.
468
10 Dateien und Verzeichnisse
' Beispiel dateien\serialize-linkedlist Module Module1 Sub Main() Dim ms As New IO.MemoryStream() Dim frmt As New _ Runtime.Serialization.Formatters.Soap.SoapFormatter() Dim test1, test2 As LinkedList ' test1: Testkette zusammenstellen, serialisieren test1 = New LinkedList("Das") test1.AddAfter("ist").AddAfter("ein").AddAfter("Satz.") frmt.Serialize(ms, test1) 'test2: Testkette deserialisieren ms.Seek(0, IO.SeekOrigin.Begin) test2 = CType(frmt.Deserialize(ms), LinkedList) ' Testausgabe Console.WriteLine("test1:") Console.WriteLine(test1.ItemsText()) Console.WriteLine("test2:") Console.WriteLine(test2.ItemsText()) End Sub End Module
'liefert 'Das ist ein Satz.' 'liefert 'Das ist ein Satz.'
Class LinkedList ... wie oo-programmierung\linkedlist3 End Class
10.10 IO-Fehler Wenn Sie (per Programmcode) mit Dateien hantieren, kann beinahe alles schief gehen: •
Sie greifen auf eine Datei zu, die es nicht mehr gibt (etwa weil die Diskette, CD-ROM, DVD etc. aus dem Laufwerk entfernt wurde).
•
Sie dürfen eine Datei nicht ändern (oder nicht einmal lesen), weil der Benutzer, der Ihr Programm ausführt, keine ausreichenden Zugriffsrechte hat.
•
Sie können auf eine Datei nicht zugreifen, weil diese von einem anderen Programm verwendet wird.
•
Die Diskette, Festplatte etc. ist voll, während Sie versuchen, eine Datei zu schreiben.
•
Der Benutzer gibt ungültige Datei- oder Verzeichnisnamen an.
•
Während der Bearbeitung einer Datei innerhalb eines lokalen Netzwerks gibt es Netzwerkprobleme.
10.10 IO-Fehler
469
VERWEIS
Diese Liste ließe sich natürlich noch fortsetzen – aber ich denke, die angeführten Punkte reichen bereits aus, um die Notwendigkeit einer guten Fehlerabsicherung zu begründen. Dieser Abschnitt gibt einen Überblick über die wichtigsten Fehler (Exceptions), die beim Umgang mit Dateien auftreten können. Ein konkretes Beispiel zur Absicherung eines einfachen IO-Programms gibt Abschnitt 10.3.2. Allgemeine Informationen darüber, wie eine Fehlerabsicherung mit Catch-Try durchgeführt werden kann, gibt Kapitel 11.
Fehlertypen (System.IO-Exceptions) Im System.IO-Namensraum sind die folgenden (in der Hierarchie fett hervorgehobenen) Exception-Klassen zur Information über Fehler vorgesehen. Die meisten der Fehler sind selbst erklärend. Hierarchie der Exception-Klassen
ACHTUNG
Exception └─ SystemException ├─ IOException │ ├─ DirectoryNotFoundException │ ├─ EndOfStreamException │ ├─ FileLoadException │ ├─ FileNotFoundException │ └─ PathTooLongException └─ InternalBufferOverflowException
Basisklasse für alle Exceptions Basisklasse für alle .NET-Exceptions Basisklasse für einige System.IO-Exceptions Verzeichnis existiert nicht. Ende der Datei wurde erreicht. Assembly-Datei kann nicht geladen werden. Datei existiert nicht. Dateiname ist zu lang (siehe unten). Pufferüberlauf (definiert in System.dll)
Beachten Sie, dass bei der Bearbeitung von Dateien auch andere Fehler auftreten können, z.B. UnauthorizedAccessException oder SecurityException, wenn Sie keine Zugriffsrechte haben, um eine Datei oder ein Verzeichnis zu bearbeiten!
Probleme mit zu langen Dateinamen Die folgenden Zeilen resultieren aus Problemen mit dem Beispielprogramm verzeichnisbaum (siehe Abschnitt 10.3.2). Dieses Programm ermittelt die Größe jeder einzelnen Datei einer Festplatte. Dabei traten bei manchen Dateien Probleme auf, die aus einem zu langen Dateinamen resultierten. (Der Vollständigkeit halber sei noch erwähnt, dass es der Internet Explorer war, der diese Dateien im Cache-Verzeichnisse Content.IE5 erzeugt hatte.) Der Hintergrund des Problems besteht darin, dass die gesamte Länge eines vollständigen Dateinamens (inklusive Laufwerks- und Verzeichnisangabe) 260 Zeichen nicht überschreiten darf. Dieses Limit wird durch System.IO vorgegeben, nicht durch das Dateisystem!
470
10 Dateien und Verzeichnisse
Die Eigenschaft FullName liefert den vollständigen Namen einer Datei oder eines Verzeichnisses. Wird dabei das 260-Zeichenlimit überschritten, tritt bei der Auswertung von FullName der Fehler System.IO.PathTooLongException auf. Falls sowohl das Verzeichnis als auch die Datei als eigenständige Objekte vorliegen (z.B. dir und file), können Sie sich damit behelfen, dass Sie den vollständigen Dateinamen mit IO.Path.Combine zusammensetzen: Dim dir As New IO.DirectoryInfo("ein verzeichnis") Dim file As IO.FileInfo Dim reallyFullName As String For Each file In dir.GetFiles("*.abc") Try reallyFullName = file.FullName Catch e As IO.PathTooLongException reallyFullName = IO.Path.Combine(dir.FullName, file.Name) End Try Console.WriteLine(reallyFullName) Next
Wenn Sie Probleme erwarten, können Sie natürlich auch auf den Try/Catch-Block verzichten und immer Combine verwenden. Der obige Code setzt allerdings voraus, dass nur file.FullName Probleme bereitet, dir.FullName aber fehlerfrei ausgeführt wird. Theoretisch kann bereits dir.FullName einen Fehler verursachen (nämlich dann, wenn bereits die aneinander gefügten Verzeichnisnamen zu lange sind), in der Praxis ist das aber relativ unwahrscheinlich. Aber selbst wenn es Ihnen gelingt, mit dem obigen Code den vollständigen Dateinamen zu ermitteln, haben Sie damit noch nicht viel gewonnen. Bei Dateien, deren Name 260 Zeichen überschreitet, besteht keine Möglichkeit, auch nur einfache Informationen (etwa die Dateigröße) zu ermitteln. Allerdings tritt in diesem Fall ein anderer Fehler auf: System.IO. FileNotFoundException. Kurz gefasst bedeutet das leider, dass Sie mit System.IO nicht alle Dateien bearbeiten können, die sich in Ihrem Dateisystem befinden.
11 Fehlersuche und Fehlerabsicherung Eine grundlegende Tatsache des Programmierens lautet leider: Jedes Programm enthält Fehler. Wie Sie Fehler in eigenen Anwendungen entdecken bzw. wie Sie Ihre Programme gegen Fehler absichern, ist Thema dieses Kapitels. Zur Fehlerabsicherung sieht VB.NET ein Konstrukt vor, das aus den Anweisungen Try, Catch und End Try besteht. Damit steht auch VB-Programmierern ein strukturierter Weg zur Absicherung ihres Codes zur Verfügung. Zur Fehlersuche bietet die Visual-Studio-Entwicklungsumgebung die besten Voraussetzungen: Sie können Variablen beobachten, den Programmcode schrittweise (Zeile für Zeile) ausführen, zwischen mehreren Threads wechseln etc. 11.1 11.2
Fehlerabsicherung Fehlersuche (Debugging)
472 490
472
11.1
11 Fehlersuche und Fehlerabsicherung
Fehlerabsicherung
Wie so oft in VB.NET gibt es auch zur Fehlerabsicherung zwei Wege. Der eine stammt aus den Zeiten von VB6 und basiert auf den diversen On-Error-xxx-Varianten. Allerdings ist dieses Konzept veraltet und führt oft zu unübersichtlichem Code. Neu in VB.NET ist die zweite Variante mit den Kommandos Try und Catch. Dieses Konzept ermöglicht es, den Code für die Fehlerbehandlung etwas besser zu strukturieren. Dieses Buch konzentriert sich auf den neuen Weg; die On-Error-Syntax wird nur kurz in Abschnitt 11.1.4 beschrieben. Am Beginn des Kapitels steht aber die Beschreibung von so genannten Ausnahmen (exceptions), die die Basis des gesamten Fehlermanagements unter VB.NET ist.
11.1.1 Ausnahmen (exceptions) Wenn Sie Code ausführen, der einen Fehler enthält, tritt dabei eine so genannte Ausnahme (exception) auf. Wenn also in diesem Kapitel von Fehlern die Rede ist, sind genau genommen immer Ausnahmen gemeint. Eine exception bewirkt, dass die normale Programmausführung unterbrochen wird und (soweit vorhanden) Code zur Fehlerbehandlung ausgeführt wird. Wenn die aktuelle Prozedur nicht gegen Fehler abgesichert ist, wird sie verlassen und die Information über den Fehler (ein Exception-Objekt) an die nächst höhere Ebene innerhalb der Aufrufhierarchie weitergegeben.
Defaultreaktion auf Fehler, Fehlerdialoge Wenn Sie das folgende Miniprogramm ausführen, tritt ein Division-durch-0-Fehler auf (System.DivideByZeroException). Sub Main() Dim a, b, c As Integer a = b \ c End Sub
Abbildung 11.1: Fehlerhaftes Programm in der Entwicklungsumgebung ausführen
11.1 Fehlerabsicherung
473
VERWEIS
In der Entwicklungsumgebung erscheint bei der Ausführung der in Abbildung 11.1 dargestellte Dialog. Mit UNTERBRECHEN wird der Debugger aktiviert und die fehlerhafte Zeile markiert. Sie können nun den Fehler analysieren (siehe Abschnitt 11.2). Wenn Sie dagegen auf WEITER drücken, wird das Programm beendet. (Warum der Button nicht klarer mit ENDE beschriftet wurde, weiß nur das Microsoft-Entwicklungsteam.) Per Default erscheint beim Auftreten jedes Fehlers, der nicht durch On Error oder Try-Catch abgesichert ist, der in Abbildung 11.1 dargestellte Dialog. Dieses Verhalten kann aber durch den Dialog DEBUGGEN|AUSNAHMEN verändert werden. In diesem Dialog können Sie erreichen, dass das Programm bei bestimmten Fehlern in jedem Fall sofort unterbrochen wird (auch wenn der Fehler abgesichert ist) oder dass der Fehler übergangen wird. Weitere Informationen zu diesem Dialog und anderen Debugging-Elementen der Entwicklungsumgebung folgen in Abschnitt 11.2.2.
Wenn Sie das obige Programm außerhalb der Entwicklungsumgebung ausführen, sieht der Fehlerdialog ein wenig anders aus (siehe Abbildung 11.2): Hier wird der Anwender gefragt, ob er (soweit verfügbar) einen Debugger zur Analyse des Problems starten möchte. JA startet den Debugger, NEIN beendet das fehlerhafte Programm. Auch dieser Dialog ist also für den Normalanwender, der von Programmierung keine Ahnung hat und den Begriff Debugger wahrscheinlich gar nicht kennt, wenig hilfreich. Sie sollten also alles tun, um zu vermeiden, dass der Endanwender einen derartigen Dialog je zu Gesicht bekommt.
Abbildung 11.2: Fehlerhaftes Programm außerhalb der Entwicklungsumgebung ausführen
474
11 Fehlersuche und Fehlerabsicherung
Wenn bei Windows-Anwendungen ein Fehler nicht unmittelbar in Ihrem eigenen Code auftritt, sondern innerhalb der Windows.Forms-Bibliothek, dann wird der in Abbildung 11.3 dargestellte Fehlerdialog angezeigt, der schon sehr viel aufschlussreicher ist (zumindest für Programmierer). Dabei ist es egal, ob es sich um einen internen Fehler oder einen von Ihnen verursachten Fehler handelt. In seltenen Fällen können Sie das Programm hier sogar mit WEITER fortsetzen, meist führt aber WEITER wie BEENDEN zum sofortigen Programmende.
Abbildung 11.3: Windows.Forms-Fehlermeldung
Exception-Eigenschaften Jede Ausnahme wird durch ein Objekt beschrieben, dessen Klasse von der Klasse System.Exception abgeleitet ist. Der folgende Baum zeigt die Hierarchie einiger der ExceptionKlassen. Hierarchie der Exception-Klassen System.Exception ├─ System.ApplicationException
│ └─ System.SystemException │ ├─ System.DivideByZeroException ├─ ... └─ System.IO.IOException │ ├─ System.IO.FileNotFoundException └─ ...
Basisklasse für alle exceptions Basisklasse für alle benutzerdefinierten exceptions Basisklasse für alle exceptions der .NET-Bibliotheken Division-durch-0-Fehler zahllose weitere Fehler Basisklasse für alle exceptions des System.IO-Namensraums Datei-nicht-gefunden-Fehler weitere System.IO-Fehler
11.1 Fehlerabsicherung
475
Die Aufgabe all dieser Objekte besteht darin, den Fehler und seine Ursache bzw. den Weg seiner Entstehung so exakt wie möglich zu beschreiben. Die folgende Tabelle fasst die wichtigsten Eigenschaften und Methoden zusammen, die bei allen Exception-Objekten zur Verfügung stehen. System.Exception-Klasse – Eigenschaften und Methoden GetBaseException()
liefert das Exception-Objekt, das am Beginn einer Kette von einander auslösenden Fehlern steht. (GetBaseException verfolgt InnerException bis an den Anfang zurück. Wenn der aktuelle Fehler nicht durch einen anderen Fehler ausgelöst wurde, liefert GetBaseException einfach Nothing.)
HelpLink
verweist auf einen zum Fehler passenden Hilfetext.
InnerException
falls der Fehler durch einen anderen Fehler ausgelöst wurde, verweist InnerException auf den vorangegangenen Fehler.
Message
enthält die Fehlerbeschreibung.
Source
gibt das Programm an, in dem der Fehler aufgetreten ist. (Per Default enthält Source einfach den Assembly-Namen.)
StackTrace
gibt an, wie es zum Aufruf der Prozedur bzw. der Methode kam, in dem der Fehler ausgelöst worden ist. (Der Inhalt von StackTrace entspricht den Informationen, die in der Entwicklungsumgebung im Fenster AUFRUFLISTE angezeigt werden.) Das folgende Beispiel gibt an, dass der Fehler in der Prozedur sub1 aufgetreten und dass sub1 von main aufgerufen worden ist. In der Praxis ist die Verschachtelung meist viel höher; die Prozedurkette enthält oft auch Prozeduren oder Methoden, die innerhalb der .NETBibliothek definiert sind. at myown_exception.Module1.sub1(String p1, Int32 p2) in D:\code\vb.net\fehler\myown-exception\Module1.vb:line 16 at myown_exception.Module1.Main() in D:\code\vb.net\fehler\myown-exception\Module1.vb:line 5
TargetSite
verweist auf ein Objekt der Klasse System.Reflection.MethodBase, das detaillierte Informationen über die Prozedur bzw. die Methode gibt, in der der Fehler aufgetreten ist. (Über das MethodBase-Objekt können Sie z.B. feststellen, welche Parameter die Methode kennt, welchen Typ diese Parameter haben etc. Die Methode ToString liefert einfach die Deklaration der Prozedur in C#-Syntax (z.B. Void sub1(System.String, Int32)).
Die von Exception abgeleiteten Klassen kennen zum Teil spezifische Zusatzeigenschaften, deren Inhalt dem Fehlertyp entspricht. Beispielsweise gibt es bei System.IO.FileNotFoundException die zusätzliche Eigenschaft FileName; sie enthält den Namen der Datei, die nicht gefunden wurde.
TIPP
476
11 Fehlersuche und Fehlerabsicherung
Im Regelfall haben Sie es also nicht mit einem Objekt der Klasse System.Exception zu tun, sondern mit einem Objekt einer davon abgeleiteten Klasse. Den Namen dieser Klasse können Sie mit e.GetType().Name ermitteln (wenn e ein allgemeines ExceptionObjekt ist).
Fehler (exceptions) selbst auslösen Der Großteil dieses Kapitels beschäftigt sich mit der Frage, wie Sie auf Fehler reagieren können, die während der Programmausführung unerwartet aufgetreten sind. Sie können Fehler (exceptions) aber auch explizit selbst auslösen. Das ist beispielsweise dann sinnvoll, wenn Sie eine eigene Klasse entwickelt haben und den Code für die einzelnen Methoden und Eigenschaften gegen unzulässige Parameter absichern möchten. Um einen Fehler auszulösen, verwenden Sie das VB.NET-Schlüsselwort Throw. Als Parameter übergeben Sie ein Exception-Objekt. Throw New Exception()
Durch Throw wird der normale Programmfluss unterbrochen. Wenn Throw innerhalb eines Try-Blocks ausgeführt wird, wird das Programm im dazugehörenden Catch-Block fortgesetzt (Details folgen im nächsten Abschnitt). Wenn der Code in der aktuellen Prozedur dagegen nicht abgesichert ist, wird die Prozedur beendet. Das Exception-Objekt wird an die übergeordnete Prozedur weitergegeben (also dorthin, wo die fehlhafte Prozedur ausgeführt wurde). Ist der Aufruf auch dort nicht durch Try abgesichert, wandert das ExceptionObjekt bis an den Ausgangspunkt der Aufrufliste. Fehlt auch dort eine Fehlerbehandlung, erscheint der am Beginn dieses Kapitels dargestellte Fehlerdialog und das Programm wird beendet. In der Regel werden Sie nicht ein allgemeines Exception-Objekt verwenden, sondern ein Objekt einer Exception-Klasse, die Ihrem Fehler besser entspricht. Um beispielsweise anzugeben, dass an einen Parameter Nothing übergeben wurde, obwohl ein konkretes Objekt erforderlich gewesen wäre, können Sie mit der folgenden Zeile eine ArgumentNullException auslösen: Throw New ArgumentNullException() ArgumentNullException ist eine der vielen durch die .NET-Bibliotheken vorgegebenen
Fehlerklassen. Zahllose weitere derartige Klassen finden Sie, wenn Sie mit dem Objektbrowser nach Klassen suchen, deren Name Exception enthält. Auf diese Weise finden Sie z.B. ArithmeticException, ConfigurationException, InvalidCastException, NotImplementedException, NullReferenceException oder SecurityException. Jede Exception-Klasse kennt eine Defaultfehlerbeschreibung (z.B. Wert darf nicht Null sein bei der ArgumentNullException-Klasse). Um statt dieses Defaulttexts einen eigenen Text anzugeben, können Sie bei den meisten Exception-Klassen an den New-Konstruktor genauere Informationen über die Art des Fehlers übergeben. Der bzw. die New-Parameter hängen von der jeweiligen Exception-Klasse ab.
11.1 Fehlerabsicherung
477
An den Konstruktor von ArgumentOutOfRangeException können beispielsweise zwei Zeichenketten übergeben werden, von denen die erste den fehlerhaften Parameter beschreibt und die zweite angibt, welche Regel verletzt wurde. Im folgenden Beispiel akzeptiert der Parameter p2 keine negative Zahlen. Die Klasse verwendet die beiden New-Parameter, um daraus die Fehlerbeschreibung zu bilden. Der Aufruf von sub1("abc", -10) führt zu einer Exception, deren Message-Eigenschaft dann so aussieht: p2 darf nicht negativ sein. Übergeben wurde -10. Parametername: p2, Prozedur sub1, Modul Module1 Public Sub sub1(ByVal p1 As String, ByVal p2 As Integer) If p2 < 0 Then Throw New ArgumentOutOfRangeException( _ "p2, Prozedur sub1, Modul Module1", _ "p2 darf nicht negativ sein. Übergeben wurde " + _ p2.ToString + ".") End If ... End Sub
Eigene Exception-Klassen Wenn die vorgegebenen Exception-Klassen Ihren Anforderungen nicht entsprechen, können Sie selbst eine Exception-Klasse programmieren. Um den Normen der .NET-Fehlerverwaltung zu entsprechen, sollte diese Klasse von System.ApplicationException abgeleitet sein. Bei der Implementierung einer eigenen Exception-Klasse müssen Sie zwei Details beachten: Sie müssen eigene Versionen für alle New-Konstruktoren der ApplicationException-Klasse schreiben.
•
Damit Fehler zwischen unterschiedlichen Rechnern übertragen werden können (was in verteilten Anwendungen manchmal notwendig ist), muss jede Exception-Klasse die Schnittstelle ISerializable implementieren. Wenn Sie in Ihrer Exception-Klasse eigene Daten speichern, müssen Sie sich selbst um deren (De-)Serialisierung kümmern. Für die Serialisierung ist die Methode GetObjectData verantwortlich, für die Deserialisierung einer der New-Konstruktoren.
VERWEIS
•
Einen lesenswerten Artikel (The Well-Tempered Exception von Eric Gunnerson) zur Programmierung eigener Exception-Klassen finden Sie hier: http://msdn.microsoft.com/library/en-us/dncscol/html/csharp08162001.asp
Alle Beispiele sind allerdings als C#-Code angegeben. Das folgende Beispiel stellt die Klasse MyOwnException vor, die sich von der gewöhnlichen Exception-Klasse durch die zusätzliche Eigenschaft ErrorTime unterscheidet. Über diese Eigenschaft kann der genaue Fehlerzeitpunkt festgestellt werden. Intern wird dieser Zeitpunkt in der Variablen _errortime gespeichert. Diese Variable wird durch die drei gewöhnlichen New-Konstruktoren mit Now initialisiert. Wird das Objekt dagegen durch den vier-
478
11 Fehlersuche und Fehlerabsicherung
ten New-Konstruktor durch eine Serialisierung erzeugt, wird der Wert von _errortime aus dem SerializationInfo-Objekt gelesen. Für die Serialisierung ist GetObjectData verantwortlich. ' Beispiel fehler\myown-exception Class MyOwnException Inherits ApplicationException Private _errortime As Date ' Defaultkonstruktor Public Sub New() Me.New("MyOwnException") ' Defaultfehlertext End Sub ' Konstruktor mit Fehlertext Public Sub New(ByVal message As String) MyBase.New(message) _errortime = Now End Sub ' Konstruktor mit Fehlertext und Angabe einer vorausgegangenen ' Exception Public Sub New(ByVal message As String, ByVal inner As Exception) MyBase.New(message, inner) _errortime = Now End Sub ' Objekt durch Deserialisierung erzeugen Public Sub New( _ ByVal info As Runtime.Serialization.SerializationInfo, ByVal context As Runtime.Serialization.StreamingContext) MyBase.New(info, context) _errortime = info.GetDateTime("errortime") End Sub ' Objekt serialisieren Public Overrides Sub GetObjectData( _ ByVal info As Runtime.Serialization.SerializationInfo, _ ByVal context As Runtime.Serialization.StreamingContext) MyBase.GetObjectData(info, context) info.AddValue("errortime", _errortime) End Sub ' Zeitpunkt des Fehlers ermitteln Public ReadOnly Property ErrorTime() As Date Get Return _errortime End Get End Property End Class
11.1 Fehlerabsicherung
479
11.1.2 Try-Catch-Syntax Um zu vermeiden, dass ein unerwartet auftretender Fehler zur Anzeige eines Fehlerdialogs und zum anschließenden Programmende führt, können Sie Ihren Code durch eine Try-Catch-Konstruktion absichern. In der einfachsten Form sieht die Syntax so aus: Try [Code, der eventuell einen Fehler auslösen könnte] Catch [Code, um auf den Fehler zu reagieren] End Try
Durch diese Konstruktion werden die Codezeilen zwischen Try und Catch ausgeführt. Wenn dabei kein Fehler auftritt, wird die Konstruktion anschließend verlassen und die nächste Anweisung nach End Try ausgeführt. Tritt hingegen irgendwo im Try-Block ein Fehler auf, wird dieser Block verlassen und der Catch-Block ausgeführt. Dieser Block eignet sich beispielsweise dazu: •
um Informationen über den Fehler in einer eigenen Dialogbox anzuzeigen,
•
um den Fehler in eine Protokolldatei einzutragen,
•
um eine E-Mail mit einem Fehlerbericht zu versenden,
•
um Objekte aus dem Speicher zu entfernen oder vergleichbare Aufräumarbeiten durchzuführen,
•
um eine neue Exception auszulösen, die Informationen über den Fehler an eine übergeordnete Prozedur weitergibt (z.B. in einer Klassenbibliothek),
•
um das Programm geordnet zu beenden.
HINWEIS
Wenn die Prozedur oder das Programm im Catch-Block nicht beendet wird, wird die Programmausführung anschließend fortgesetzt, als wäre nie ein Fehler aufgetreten. Der Fehler (die exception) gilt durch die Ausführung des Catch-Codes also automatisch als behoben bzw. als ausreichend verarbeitet – vollkommen unabhängig davon, was Ihr Programm im Catch-Code tatsächlich tut. Die Try-Catch-Syntax setzt voraus, dass innerhalb der Konstruktion zumindest ein Catch- oder Finally-Block (siehe unten) angegeben wird. Es ist aber erlaubt, dass der Catch-Block leer ist (d.h., die nächste Catch folgende Zeile lautet End Try). Das bewirkt, dass der Fehler ohne Fehlermeldung einfach ignoriert wird und die Codeausführung nach End Try fortgesetzt wird, als wäre nichts gewesen. Es liegt auf der Hand, dass eine derartige Absicherung selten sinnvoll ist und im Gegenteil das Auftreten von Fehlern nur verschleiert (ähnlich wie durch On Error Resume Next in VB6).
VORSICHT
480
11 Fehlersuche und Fehlerabsicherung
Fehler, die innerhalb des Catch-Blocks auftreten, sind durch die Try-Catch-Konstruktion nicht abgesichert! Stellen Sie also beim Programmieren sicher, dass Ihr CatchCode garantiert fehlerfrei ausgeführt wird, oder sichern Sie diesen Code zur Not durch eine verschachtelte Try-Catch-Konstruktion ab. (Allzu weit sollten Sie diese Art der Verschachtelung aber nicht treiben ...)
Differenzierte Reaktion auf Fehler In der obigen Form können Sie im Catch-Block nicht auf das Exception-Objekt zugreifen, das den Fehler beschreibt. Dieses Objekt benötigen Sie aber, wenn Sie eine differenzierte Fehlerbehandlung realisieren möchten. Dabei hilft die folgende Syntaxvariante: Try [Code, der eventuell einen Fehler auslösen könnte] Catch e As Exception [Code, um auf den Fehler zu reagieren] End Try
Nun können Sie innerhalb des Catch-Blocks über die lokale Variable e auf alle Eigenschaften des zum Fehler gehörenden Exception-Objekts zugreifen. Die folgenden Zeilen geben dazu ein konkretes Beispiel. Dim txt As String Dim sr As IO.StreamReader Try sr = New IO.StreamReader("diese_datei_gibt_es_nicht.txt") txt = sr.ReadToEnd() sr.Close() Console.WriteLine(txt) Catch e As Exception ' Datei schließen If Not IsNothing(sr) Then sr.Close() ' Informationen über den Fehler anzeigen Console.WriteLine("Exception-Name: " + e.GetType.Name) Console.WriteLine("Fehlertext: " + e.Message) Console.WriteLine("Fehlerort: " + e.StackTrace) End Try
Als weitere Syntaxvariante können mehrere Catch-Blöcke für unterschiedliche ExceptionKlassen angegeben werden. Wenn ein Fehler auftritt, werden alle Catch-Anweisungen der Reihe nach überprüft, bis eine Catch-Anweisung gefunden wird, die auf den Fehler zutrifft.
11.1 Fehlerabsicherung
481
Try sr = New IO.StreamReader("diese_datei_gibt_es_nicht.txt") txt = sr.ReadToEnd() sr.Close() Console.WriteLine(txt) Catch e As IO.FileNotFoundException ' Datei wurde nicht gefunden ... Catch e As Exception ' ein anderer Fehler ist aufgetreten ... End Try
Optional können Sie jede Catch-Anweisung mit When bedingung ergänzen. Dadurch wird der nachfolgende Codeblock nur ausgeführt, wenn die Bedingung erfüllt ist.
HINWEIS
Beachten Sie, dass Sie unbedingt zuerst spezifische und erst dann allgemeine Exception-Klassen angeben müssen (also in der Vererbungshierarchie der Exception-Klassen zuerst die abgeleiteten und erst dann die Basisklassen)! Da jede Exception-Klasse von System.Exception abgeleitet ist, trifft Catch e As Exception auf jeden Fehler zu. Wenn Catch e As Exception also am Beginn der Catch-Alternativen steht, trifft diese Variante immer zu und die anderen Catch-Alternativen werden gar nicht mehr berücksichtigt. Catch e As Exception sollte daher am Ende der Catch-Variante angegeben werden. Damit gilt der letzte Catch-Block als Sammelbecken für alle Fehler, deren genauen
Typ Sie nicht vorhergesehen haben bzw. die Sie nicht separat berücksichtigen wollten. Wenn innerhalb der Try-Catch-Konstruktion ein Fehler auftritt, der keiner der CatchVarianten entspricht, gilt dieser Fehler als unbehandelt und es kommt zur Anzeige der am Beginn dieses Kapitels abgebildeten Defaultfehlermeldung und zum anschließenden Programmende.
Finally-Block Am Ende der Try-Catch-Konstruktion kann ein Finally-Block angegeben werden. Der darin enthaltene Code wird sowohl nach der fehlerfreien Ausführung des Try-Blocks als auch nach dem Auftreten eines behandelten Fehlers im Anschluss an den Catch-Block abgearbeitet. Wenn dagegen ein unbehandelter Fehler auftritt, wird die gesamte Try-Catch-Konstruktion verlassen und zumeist auch der Finally-Block ignoriert.
482
11 Fehlersuche und Fehlerabsicherung
Try [Code, der eventuell einen Fehler auslösen könnte] Catch ... [Code, um auf den Fehler zu reagieren] Finally [Code, der immer ausgeführt wird] End Try
Syntaktisch gesehen ist der Finally-Block weitgehend sinnlos. Sie können den Code des Finally-Blocks ebenso gut nach End Try angeben, also: Try [Code, der eventuell einen Fehler auslösen könnte] Catch ... [Code, um auf den Fehler zu reagieren] End Try [Code, der immer ausgeführt wird]
HINWEIS
Die Berechtigung des Finally-Blocks besteht in erster Linie wohl darin, dass logisch zusammengehörender Code damit vollständig innerhalb der Try-Catch-Konstruktion angeordnet werden kann; das kann die Lesbarkeit des Programms ein wenig verbessern. Es gibt einige wenige Exceptions, die vom gewöhnlichen Verhalten abweichen. Dazu zählt die Threading.ThreadAbortException: Wenn diese Exception innerhalb eines TryBlocks auftritt, wird der Finally-Block selbst dann ausgeführt, wenn die Exception nicht in einem Catch-Block abgefangen wird bzw. wenn es gar keinen Catch-Block gibt. Weitere Informationen zu dieser Exception folgen in Abschnitt 12.6.2.
Syntaxzusammenfassung Try-Catch-Syntax Try ...
führt die Codezeilen bis zur ersten Catch-Anweisung aus. Wenn dabei kein Fehler auftritt, wird der (optionale) Finally-Block ausgeführt.
Catch e As xxxException [When cond] falls im Try-Block ein Fehler des Typs xxxException ... aufgetreten ist (und die optionale Bedingung cond
erfüllt ist), wird zuerst dieser Block und dann der Finally-Block ausgeführt. Über die lokale Variable e kann auf das Exception-Objekt zugegriffen werden. Catch [e As Exception] ...
berücksichtigt alle Fehler, die bisher nicht explizit behandelt wurden.
Finally ...
wird in jedem Fall ausgeführt, unabhängig davon, ob ein behandelter Fehler aufgetreten ist oder nicht.
End Try
beendet die Try-Catch-Konstruktion.
11.1 Fehlerabsicherung
483
11.1.3 Programmiertechniken Minimalabsicherung für Konsolenanwendungen Try/Catch gilt auch für alle untergeordneten Prozeduren, die Sie aufrufen. Wenn es sich
beim folgenden Programm um eine Konsolenanwendung handelt, deren gesamter Code über myFunction aufgerufen wird, so ist das gesamte Programm abgesichert – wenn auch nur notdürftig. Sobald in myFunction oder in irgendeiner anderen Prozedur, die von dort aus ausgerufen wurde, ein Fehler auftritt, wird der Catch-Block von Main ausgeführt. Dort können Sie dann eine Fehlermeldung anzeigen und das Programm geordnet beenden. Sub Main() Try myFunction() Catch MsgBox("error") End Try End Sub
Absicherung von eigenen Klassen Schon wesentlich aufwendiger ist es, eine selbst programmierte Klasse abzusichern. Es gibt leider keine Möglichkeit, eine zentrale Fehlerbehandlungsprozedur einzurichten, die automatisch immer dann aufgerufen wird, wenn in irgendeinem Codeabschnitt der Klasse ein nicht abgesicherter Fehler auftritt. Daher müssen Sie jede Prozedur (Methode, Eigenschaft etc.) der Klasse einzeln absichern, was bei umfangreichen Klassen natürlich sehr mühsam ist.
Absicherung von Windows-Programmen Jedes Formular (Fenster) eines Windows-Programms ist eine eigene Klasse. Daher gibt es keinen Unterschied zwischen der Absicherung eines Windows-Programms und der einer Klasse: Sie müssen jede Prozedur (und insbesondere jede Ereignisprozedur, die als Reaktion auf Benutzereingaben, Mausklicks etc. aufgerufen wird) explizit absichern. Damit Sie nicht immer wieder denselben Code zur Fehlerabsicherung in den Catch-Blöcken der diversen Prozeduren angeben müssen, können Sie eine eigene Prozedur zur Fehlerbehandlung aufrufen. ' jede (Ereignis-)Prozedur einzeln absichern Sub prozedur1, 2, 3, 4 ...() Try ... normaler Code Catch e As Exception myErrorCode(e) End Try End Sub
484
11 Fehlersuche und Fehlerabsicherung
ACHTUNG
Function myErrorCode(e As Exception) As Integer .. Code zur Fehlerabsicherung und evt. zum Programmende MsgBox("error") End Function
Je nach Anwendung können Sie dem Anwender in myErrorCode die Möglichkeit geben, das Programm zu beenden oder zu versuchen, es fortzusetzen. Wenn das Programm in myErrorCode nicht beendet wird, wird es bei der Zeile End Try in der den Fehler verursachenden Prozedur fortgesetzt. Das ist aber nicht immer sinnvoll. Ein differenzierteres Verhalten können Sie beispielsweise erreichen, indem Sie die weitere Programmausführung vom Rückgabewert der myErrorCode-Funktion abhängig machen.
Fehlerabsicherung absichern Wenn innerhalb eines Catch-Blocks ein weiterer Fehler auftritt, gilt dieser Fehler als ganz normaler Fehler, der zu einer Programmunterbrechung führt! Sie können aber natürlich wahlweise den Catch-Block durch eine weitere Try-Konstruktion (Variante 1) oder den gesamten Try-Block zweifach absichern (Variante 2). ' Variante 1 Try ... normaler Code Catch Try ... Code zur Fehlerbehandlung Catch ... Code, wenn in der Fehlerbehandlung ein Fehler auftritt End Try End Try ' Variante 2 Try Try ... normaler Code Catch ... Code zur Fehlerbehandlung End Try Catch ... Code, wenn in der Fehlerbehandlung ein Fehler auftritt End Try
11.1 Fehlerabsicherung
485
Absicherung von IDisposable-Objekten Wenn Sie Objekte erzeugen, deren Klasse die IDisposable-Schnittstelle implementieren, sollten Sie bei der Fehlerabsicherung darauf achten, dass das Objekt ordnungsgemäß aus dem Speicher entfernt wird. Die folgenden Zeilen geben hierfür ein Beispiel. (Sowohl die Bitmap- als auch die Graphics-Klasse sind IDisposable-Klassen.) Dim bm As Drawing.Bitmap Dim gr As Drawing.Graphics Try bm = New Drawing.Bitmap(100, 100) gr = Drawing.Graphics.FromImage(bm) gr.DrawLine(Drawing.Pens.Black, 0, 0, 50, 50) Catch If Not IsNothing(gr) Then gr.Dispose() If Not IsNothing(bm) Then bm.Dispose() End Try
Exception-Objekt modifizieren oder weitergeben Was ist eigentlich das Ziel der Fehlerabsicherung? Bei interaktiven Programmen soll zumeist eine verständliche Fehlermeldung angezeigt werden. Außerdem sollte dem Anwender eine Möglichkeit gegeben werden, das Programm entweder fortzusetzen oder es zumindest kontrolliert zu beenden. Wenn Sie dagegen eigene Klassen entwickeln, die möglicherweise von nichtinteraktiven Programmen genutzt werden sollen (z.B. von Server-Diensten), sieht die Zielsetzung ganz anders aus: Soweit der Fehler innerhalb des Fehlerbehandlungscodes nicht umgangen werden kann, soll eine möglichst präzise Information über den Fehler in Form eines Exception-Objekts an das Programm zurückgegeben werden, das die Klasse nutzt. Dabei gibt es wiederum zwei Möglichkeiten, wie ein bereits aufgetretener Fehler innerhalb des Fehlerabsicherungscodes weitergegeben werden kann: Entweder indem das ExceptionObjekt durch Throw unverändert neu ausgelöst wird; oder indem ein neues Exception-Objekt erzeugt wird, das zusätzliche Kontextinformationen zum Fehler angibt und via InnerException auf den ursprünglichen Fehler verweist. Das folgende Beispiel veranschaulicht beide Vorgehensweisen. class1 ist eine Klasse, in der zwei Integer-Werte x und y gespeichert werden. Die Methode quot berechnet den Quotienten der beiden Zahlen. Wenn dabei eine Division durch 0 auftritt, wird der Sonderfall x=0 und y=0 berücksichtigt: In diesem Fall wird statt eines Fehlers das Resultat 1 zurückgegeben. In allen anderen Fällen wird der Fehler durch Throw einfach neu ausgelöst. (Wenn in quot ein anderer Fehler auftritt, der durch Catch DivideByZeroException nicht erfasst wird, wird dieser Fehler automatisch an die aufrufende Stelle im Code weitergegeben, ohne dass hierfür eigener Code erforderlich ist.)
486
11 Fehlersuche und Fehlerabsicherung
' Beispiel fehler\modify-exception Class class1 Public x, y As Integer Public Sub New(ByVal x As Integer, ByVal y As Integer) Me.x = x Me.y = y End Sub Public Function quot() As Integer Try Return x \ y Catch e As DivideByZeroException If x = 0 And y = 0 Then ' für diese Klasse gilt: 0 / 0 = 1 Return 1 Else ' Exception neuerlich auslösen Throw e End If End Try End Function End Class class2 hat dieselbe Aufgabe wie class1 und unterscheidet sich nur durch die Fehlerabsicherung: Der Sonderfall x=0 und y=0 wird hier bereits vor der Division kontrolliert und führt
(ohne einen Fehler auszulösen) zur Rückgabe von 1. Sollte dagegen bei der Integerdivision x \ y ein beliebiger Fehler auftreten, dann wird ein neues Exception-Objekt erzeugt und der Fehler mit Throw ausgelöst. Dabei werden als Message-Eigenschaft des Objekts einige Kontextinformationen übergeben: der Ort des Fehlers und der Inhalt von x und y. Außerdem wird das ursprüngliche Exception-Objekt als zweiter New-Parameter übergeben. Es kann dann aus der InnerException-Eigenschaft des neuen Exception-Objekts gelesen werden. Class class2 '[ ... x, y und New() wie in class1 ...] Public Function quot() As Integer Try ' für diese Klasse gilt: 0 / 0 = 1 If x = 0 And y = 0 Then Return 1 Return x \ y Catch e As Exception ' Exception neuerlich auslösen Dim msg As String msg = String.Format( _ "Fehler in Methode class2.quot. x={0}, y={1}", x, y) Throw New Exception(msg, e) End Try End Function End Class
11.1 Fehlerabsicherung
487
Die folgenden Zeilen zeigen die (abgesicherte) Anwendung der beiden Klassen. Zur Anzeige der Fehlertexte wird die Funktion AllExceptionMessages verwendet, die mit InnerException alle Fehler bis zur ursprünglichen Exception durchläuft. Module Module1 Sub Main() Dim a As New class1(0, 0) Console.WriteLine("a.quot() = {0}", a.quot())
'kein Fehler
Dim b As New class1(1, 0) Try Console.WriteLine("b.quot() = {0}", b.quot()) Catch e As Exception Console.WriteLine(AllExceptionMessages(e)) End Try Dim c As New class2(1, 0) Try Console.WriteLine("c.quot() = {0}", c.quot()) Catch e As Exception Console.WriteLine(AllExceptionMessages(e)) End Try End Sub Public Function AllExceptionMessages(ByVal e As Exception) As String Dim msg As String Dim indent As Integer Dim innerex As Exception = e.InnerException msg = e.GetType().Name + ": " + e.Message While Not IsNothing(innerex) indent += 2 msg += vbCrLf + Space(indent) msg += innerex.GetType().Name + ": " + innerex.Message innerex = innerex.InnerException End While msg += vbCrLf Return msg End Function End Module
Das Beispielprogramm liefert folgendes Ergebnis: a.quot() = 1 DivideByZeroException: Es wurde versucht, durch null zu teilen. Exception: Fehler in Methode class2.quot. x=1, y=0 DivideByZeroException: Es wurde versucht, durch null zu teilen.
488
11 Fehlersuche und Fehlerabsicherung
11.1.4 On-Error-Syntax
HINWEIS
Zur Fehlerabsicherung können Sie statt Try-Catch-Konstruktionen auch die aus VB6 stammende On-Error-Anweisungen verwenden. Der Nachteil des On-Error-Konzepts besteht darin, dass es auf Goto-Kommandos basiert, also auf Sprüngen im Code. Das führt oft zu einem schwer nachvollziehbaren Codeablauf, weswegen Sie nach Möglichkeit auf OnError-Anweisungen verzichten sollten. Innerhalb einer Prozedur können Sie entweder On Error oder Try-Catch zur Fehlerabsicherung verwenden, aber nicht beide Konstruktionen gleichzeitig!
Das Grundkonzept von On Error ist in der Prozedur sub1 demonstriert: Die Anweisung On Error Goto label bewirkt, dass das Programm beim Auftreten eines Fehlers beim angegebenen Sprunglabel fortgesetzt wird. (Ein Sprunglabel wird durch einen Text mit einem nachfolgenden Doppelpunkt gekennzeichnet. Der Label muss sich innerhalb der Prozedur befinden.) Im Fehlerbehandlungscode können die Details des Fehlers aus einem Objekt der Klasse Microsoft.VisualBasic.ErrObject gelesen werden. Dieses Objekt ist über die Funktion Err zugänglich. Err.Number liefert eine VB6-kompatible Fehlernummer. (Err.GetException liefert übrigens das zum Fehler passende Exception-Objekt.)
Die Prozedur kann nun im Fehlerbehandlungscode verlassen werden – damit gilt der Fehler als behandelt. Sie können aber auch Resume ausführen, um die Prozedur an einem beliebigen weiteren Label fortzusetzen. (Dabei ist Vorsicht geboten: Wenn der Fehler nun abermals auftritt, gerät das Programm in eine Endlosschleife.) Als Variante zu Resume können Sie auch Resume Next ausführen, um das Programm in der Zeile nach der fehlerhaften Zeile fortzusetzen. ' Beispiel Sub sub1() Dim a, b, c As Integer On Error GoTo errorcode Console.WriteLine("in sub1:") tryagain: a = b \ c Console.WriteLine("a={0}", a) Exit Sub errorcode: ' Fehlerbehandlung Console.WriteLine(Err.Description) c = 1 Resume tryagain End Sub
11.1 Fehlerabsicherung
489
Noch verpönter als On Error Goto ist in VB-kritischen Kreisen die Anweisung On Error Resume Next. Diese Anweisung bewirkt, dass das Auftreten von Fehlern einfach ignoriert wird. Stattdessen wird das Programm mit der nächsten Zeile fortgesetzt. Wenn Sie möchten, können Sie am Ende des kritischen Blocks Err überprüfen, ob in der Zwischenzeit ein Fehler aufgetreten ist. Sub sub2() Dim a, b, c As Integer On Error Resume Next Console.WriteLine("in sub2:") a = b \ c Console.WriteLine("a={0}", a) ' Fehlerbehandlung If Not IsNothing(Err) Then Console.WriteLine(Err.Description) End If End Sub
Syntaxzusammenfassung On-Error-Syntax On Error Resume Next
ignoriert Fehler und setzt das Programm mit der nächsten Anweisung fort.
On Error Goto label
setzt das Programm beim Auftreten eines Fehlers an der Sprungmarke label fort.
Resume label
setzt das Programm bei der Sprungmarke label fort. Resume kann nur innerhalb des Fehlerbehandlungscodes ausgeführt werden.
Resume Next
setzt das Programm mit der nächsten Anweisung nach der fehlerhaften Anweisung fort. Auch Resume kann nur innerhalb des Fehlerbehandlungscodes ausgeführt werden.
Err
verweist auf ein Objekt der Klasse ErrObject, das Informationen über den Fehler enthält.
Err.GetException()
verweist auf das Exception-Objekt des Fehlers.
Err.Number
liefert eine VB6-kompatible Fehlernummer.
490
11.2
11 Fehlersuche und Fehlerabsicherung
Fehlersuche (Debugging)
Der Begriff Debugging bezeichnet die Suche nach Fehlern in einem Programm. Debugging betreiben Sie immer dann, wenn Ihr Programm nicht so funktioniert, wie Sie es erwarten, und Sie auf der Suche nach den Ursachen sind. Dieser Abschnitt gibt einen Überblick über die zu diesem Zweck vorgesehenen Hilfsmitteln in VB.NET und über den in die Entwicklungsumgebung integrierten Debugger. (Der Debugger ist kein isolierter Teil der Entwicklungsumgebung, sondern ein integrierter Bestandteil, der beispielsweise das zeilenweise Ausführen von Code ermöglicht.)
11.2.1 Grundlagen Debug- oder Release-Kompilat Die Debugging-Eigenschaften eines Programms hängen stark davon ab, ob es als Debugoder als Release-Kompilat erstellt wird. Wenn Sie Ihr Programm in der Entwicklungsumgebung erstellen, wird per Default die Debug-Variante erzeugt. Zwischen Debug- und ReleaseVariante können Sie mit ERSTELLEN|KONFIGURATIONSMANAGER, in den Projekteigenschaften oder mit in der Standardsymbolleiste wählen. Dieser Abschnitt setzt voraus, dass Sie zur Fehlersuche die Debug-Variante verwenden. Durch welche Eigenschaften sich das Debug- und Release-Kompilat unterscheiden, können Sie im Dialogblatt KONFIGURATIONSEIGENSCHAFTEN|ERSTELLEN der Projekteigenschaften einstellen (siehe Abbildung 11.4). Per Default unterscheidet sich ein Debug-Kompilat in folgenden Punkten von einem Release-Kompilat: •
Zusammen mit dem Kompilat wird eine zusätzliche Datei name.pdb erzeugt, die Debugging-Informationen enthält. Diese Informationen ermöglichen es dem in der Entwicklungsumgebung integrierten Debugger, Unterbrechungen bei der Programmausführung einer bestimmten Zeile zuzuordnen. Darüber hinaus können Sie das Programm im Debugger zeilenweise ausführen etc. Die Debugging-Datei ist also eine Grundvoraussetzung dafür, dass Sie den Debugger zur Fehlersuche verwenden können.
•
Die Konstante Debug wird vordefiniert. Diese Konstante kann durch #If debug Then ... ausgewertet werden, beispielsweise um einen Codeblock nur dann auszuführen, wenn das Programm in der Debug-Konfiguration kompiliert wurde. Gleichzeitig können alle Methoden und Eigenschaften der Klasse Debug verwendet werden. (Bei Release-Kompilaten werden alle Debug.Xxx-Anweisungen einfach ignoriert.)
Daneben können aber eine Reihe weitere Eigenschaften unterschiedlich eingestellt werden, z.B. das Verzeichnis, in dem das Kompilat erstellt wird, vordefinierte Konstanten etc.
TIPP
11.2 Fehlersuche (Debugging)
491
Im Konfigurationsmanager können Sie neben Debug und Release weitere Konfigurationen (Profile) definieren und deren Eigenschaften dann im Projekteigenschaftendialog einstellen.
Abbildung 11.4: Die Debug-Konfiguration im Projekteigenschaftendialog
Programmausführung unterbrechen
VERWEIS
Wenn Sie ein Programm in der Entwicklungsumgebung ausführen, können Sie die Ausführung mit DEBUGGEN|UNTERBRECHEN oder mit Strg+Untbr unterbrechen. Bei Konsolenanwendungen hat Strg+C dieselbe Wirkung. Diese beiden Tastenkombinationen sind beispielsweise dann praktisch, wenn sich Ihr Programm in einer Endlosschleife befindet. Bei Release-Kompilaten bewirken Strg+Untbr und Strg+C ein sofortiges Programmende. Es scheint keine Möglichkeit zu geben, Strg+Untbr oder Strg+C durch Catch-Try abzufangen. Zu Programmunterbrechungen kommt es auch, wenn im Programm ein unbehandelter Fehler (eine Exception) auftritt oder wenn im Programm ein Haltepunkt gesetzt ist. Exceptions wurden im vorigen Abschnitt dieses Kapitels ausführlich beschrieben, Informationen zu Haltepunkten folgen etwas weiter unten.
492
11 Fehlersuche und Fehlerabsicherung
Code verändern Wie in Kapitel 3 bereits beschrieben wurde, unterscheidet sich VB.NET in einem ganz elementaren Punkt von den Vorgängerversionen VB1 bis VB6 und VBA: Es ist nicht mehr möglich, in einem unterbrochenen Programm Code zu verändern. (Sie können die Entwicklungsumgebung zwar so einstellen, dass Änderungen erlaubt sind, diese werden aber erst wirksam, wenn das Programm neu kompiliert und gestartet wird.)
11.2.2 Fehlersuche in der Entwicklungsumgebung Haltepunkte Indem Sie eine Programmzeile am linken Rand anklicken, setzen Sie in dieser Zeile einen so genannten Haltepunkt (break point). In der Entwicklungsumgebung werden Haltepunkte durch einen roten Kreis neben der Programmzeile angezeigt. Wenn die mit einem Haltepunkt versehene Zeile bei der Programmausführung erreicht wird, wird das Programm unterbrochen. Sie haben nun die Möglichkeit, den Inhalt einzelner Variablen zu analysieren oder das Programm (eventuell nur zeilenweise) fortzusetzen. Neben einfachen Haltepunkten kennt die Entwicklungsumgebung auch Haltepunkte, die nur dann berücksichtigt werden, wenn eine bestimmte Bedingung zutrifft, oder nur dann, wenn die Zeile n Mal erreicht wird. Derartige Zusatzbedingungen können im Dialog HALTEPUNKTEIGENSCHAFTEN eingestellt werden. Dieser Dialog ist über das Kontextmenü des Haltepunkts oder über das Haltepunktfenster erreichbar. Das Haltepunktfenster (siehe Abbildung 11.5) hilft bei der Verwaltung aller Haltepunkte.
TIPP
Abbildung 11.5: Das Haltepunktfenster zur Verwaltung aller Haltepunkte
Wenn Haltepunkte in der Entwicklungsumgebung mit einem Fragezeichen angezeigt werden (Tooltip-Text: Der Haltepunkt wird momentan nicht erreicht. Für dieses Dokument wurden keine Symbole geladen.), dann haben Sie Ihr Programm in der Release-Konfiguration kompiliert. Haltepunkte werden aber nur in der Debug-Konfiguration unterstützt. Stellen Sie also auf die Debug-Konfiguration um und kompilieren Sie Ihr Programm neu!
11.2 Fehlersuche (Debugging)
493
Programm zeilenweise ausführen Nach einer Unterbrechung können Sie das Programm zeilenweise (also Anweisung für Anweisung) fortsetzen. Dazu können Sie entweder die Kommandos des DEBUGGEN-Menüs, die dort angegebenen Tastenkürzel oder die Buttons der DEBUGGEN-Symbolleiste verwenden. Die folgende Aufzählung beschreibt die verschiedenen Kommandos, die dabei zur Auswahl stehen: •
STARTEN/FORTSETZEN: setzt die Programmausführung unbegrenzt fort (bis der nächste
Fehler auftritt bzw. der nächste Haltepunkt erreicht wird). •
EINZELSCHRITT: führt nur die nächste Anweisung aus.
•
PROZEDURSCHRITT: führt ebenfalls nur die nächste Anweisung aus. Wenn es sich dabei
um den Aufruf einer selbst definierten Methode oder Prozedur handelt, wird diese Prozedur vollständig ausgeführt (und nicht Anweisung für Anweisung wie bei EINZELSCHRITT). •
AUSFÜHRUNG BIS RÜCKSPRUNG: führt die Prozedur bis an ihr Ende aus. Die Programmaus-
führung wird bei der nächsten Anweisung nach dem Aufruf der Prozedur wieder unterbrochen. •
AUSFÜHREN BIS CURSOR: führt die Prozedur aus, bis die Anweisung an der aktuellen Cursorposition erreicht ist.
TIPP
Obwohl es im Menü der Entwicklungsumgebung und in den Symbolleisten wirklich nicht an Kommandos mangelt, die man nie braucht, fehlt per Default ein Teil der hier beschriebenen Kommandos. Das können Sie mit EXTRAS|ANPASSEN aber schnell ändern. Das BEFEHLE-Dialogblatt enthält die vollständige Liste aller Debugging-Kommandos, die Sie einfach per Drag&Drop in das Menü einbauen können.
TIPP
Darüber hinaus können Sie mit NÄCHSTE ANWEISUNG FESTLEGEN den Punkt für die Programmfortsetzung beliebig festlegen. (Sie können also beispielsweise den Cursor an das Ende einer Schleife setzen, dort die NÄCHSTE ANWEISUNG FESTLEGEN und das Programm dann an dieser Stelle durch EINZELSCHRITT fortsetzen.)
In der Praxis werden Sie die oben beschriebenen Kommandos meistens per Tastatur ausführen. Leider sind die Tastenkürzel stark von der Konfiguration der Entwicklungsumgebung abhängig, weswegen ich hier auf eine Referenz verzichte. Die gerade gültigen Tastenkürzel werden im DEBUGGEN-Menü angezeigt.
Befehlsfenster Während das Programm unterbrochen ist, können Sie im Befehlsfenster (ANSICHT|ANDERE FENSTER|BEFEHLSFENSTER) einfache VB-Kommandos ausführen. Das Fenster eignet sich inbesondere dazu, um Prozeduren aufzurufen oder den Wert einer Variablen mit ? anzuzeigen.
VERWEIS
494
11 Fehlersuche und Fehlerabsicherung
VB-Kommandos können nur dann ausgeführt werden, wenn sich das Befehlsfenster im so genannten unmittelbaren Modus befindet. Wenn das nicht der Fall ist, gelangen Sie mit dem Kommando immed in diesen Modus (siehe auch Abschnitt 1.3.6).
Überwachungsfenster Während das Programm unterbrochen ist, wollen Sie normalerweise wissen, welchen Inhalt die gerade aktuellen Variablen haben. Die Entwicklungsumgebung stellt gleich eine ganze Reihe von Fenstern zur Auswahl, um diese Informationen zu ermitteln. Diese Fenster können mit DEBUGGEN|FENSTER|NAME geöffnet werden: •
AUTO: Das Fenster (siehe Abbildung 11.6) zeigt die Inhalte aller Variablen an, die in der aktuellen Zeile und in den umgebenden Anweisungen benutzt werden.
•
LOKAL: Dieses Fenster zeigt alle Variablen an, die innerhalb der aktuellen Prozedur lokal definiert sind. (Dazu zählen auch alle Parameter der Prozedur.)
•
ME: Das Fenster zeigt nur das Objekt Me an, das auf die aktuelle Objektinstanz verweist.
•
ÜBERWACHEN 1 bis 4: In diese vier Fenster können manuell einzelne Variablen zur Über-
wachung eingefügt werden. (Dazu klicken Sie die Variable mit der rechten Maustaste an und führen das Kommando ÜBERWACHUNG HINZUFÜGEN aus.) •
SCHNELLÜBERWACHUNG: Dieser Dialog zeigt nur den Inhalt einer einzigen Variablen an. Der Dialog kann durch Anklicken der Variablen mit der rechten Maustaste über ein Kontextmenükommando geöffnet werden.
Alle Fenster zeichnen sich durch einige gemeinsame Merkmale aus: So werden die Inhalte von Variablen, die sich durch die letzte Anweisung geändert haben, rot dargestellt. Zahlenwerte können per Kontextmenü wahlweise dezimal oder hexadezimal angezeigt werden. Vor Objekten wird ein Pluszeichen angezeigt, mit dem das Objekt auseinandergeklappt werden kann. Damit können die Eigenschaften von Objekten angezeigt werden. Wenn diese auf andere Objekte verweisen, wiederholt sich die Vorgehensweise, so dass ein ganzer Objektbaum angezeigt werden kann. (Das wird allerdings rasch sehr unübersichtlich.)
Aufrufliste Das Fenster AUFRUFLISTE zeigt an, wie die aktuelle Codeposition erreicht worden ist. In Abbildung 11.7 wurde beispielsweise in Module1.Main die Methode ItemsText eines Objekts der Klasse LinkedList ausgeführt. Innerhalb des Codes dieser Methode wurde wiederum die Eigenschaft Value gelesen (was im Aufruffenster als get_Value dargestellt wird).
11.2 Fehlersuche (Debugging)
495
Abbildung 11.6: Das Auto-Fenster mit den Variablen, die in der aktuellen Anweisung und den umliegenden Anweisungen benutzt werden
Durch einen Doppelklick innerhalb der Aufrufliste können Sie den aktuellen Gültigkeitsbereich (Kontext) verändern. Wenn Sie den Kontext beispielsweise auf Main() schalten, können Sie feststellen, welchen Inhalt die Variablen in Main hatten, als die Methode ItemsText ausgeführt worden ist. (Das Programm kann aber unabhängig vom Kontext nur in der höchsten Ebene der Aufrufliste fortgesetzt werden.) Bei Ereignisprozeduren von Windows-Anwendungen oder bei rekursiven Algorithmen kann die Aufrufliste sehr lang sein. Außerdem kann die Aufrufliste Prozedur- und Methodenaufrufe von .NET-Klassen enthalten. Derartige Einträge sind in grauer Schrift dargestellt. Der dazugehörige Code kann dann nur als Assembler-Code angezeigt werden; dieser Code ist aber nur interessant, wenn Sie über ein umfassendes Assembler- und .NETHintergrundwissen verfügen.
Abbildung 11.7: Die Aufrufliste zeigt die Hierarchie der Prozedur- und Methodenaufrufe an
496
11 Fehlersuche und Fehlerabsicherung
Threads-Fenster Wenn Sie Multithreading-Anwendungen erstellen, können Sie im Threads-Fenster zwischen den Threads Ihres Programms wechseln und das Programm gezielt in einem bestimmten Thread fortsetzen. Außerdem können Sie dort einen Thread sperren. Das bedeutet, dass dieser Thread nicht mehr ausgeführt wird, bis die Sperre aufgehoben wird.
Ausnahmen-Fenster Per Default wird das Programm bei jeder nicht abgesicherten Ausnahme unterbrochen. Dieses Verhalten können Sie im Dialog DEBUGGEN|AUSNAHMEN verändern. Dort können Sie für jede Exception-Klasse bzw. für jede Gruppe derartiger Klassen (z.B. für alle System.IOFehler) festlegen, wie sich der Debugger verhalten soll, wenn ein Fehler auftritt. Dabei gibt es folgende Varianten: •
Beim Auftreten des Fehlers kann das Programm sofort unterbrochen werden. (Das ist die Defaulteinstellung für die Gruppe Common Language Runtime Exceptions.)
•
Der Fehler wird vorerst ignoriert.
•
Es gilt die Einstellung der übergeordneten Gruppe. (Das ist die Defaulteinstellung für fast alle Fehler.)
Sofern der Fehler beim Auftreten ignoriert wurde (und nur dann), kann in der zweiten Optionsgruppe eingestellt werden, wie sich der Debugger verhalten soll, wenn der Fehler nicht durch On-Error oder Catch-Try abgesichert wurde: Der Debugger wird jetzt gestartet. (Das ist die Defaulteinstellung für die Gruppe Common Language Runtime Exceptions.)
•
Der Fehler wird weiterhin ignoriert.
•
Es gilt die Einstellung der übergeordneten Gruppe. (Das ist die Defaulteinstellung für fast alle einzelnen Fehler.) HINWEIS
•
In den obigen Punkten wurden auch die Defaulteinstellungen für die meisten Exceptions beschrieben. Beachten Sie aber, dass es vereinzelte Ausnahmen gibt! Eine davon ist Threading.ThreadAbortException. Dort sind beide Optionsfelder auf WEITER voreingestellt (siehe auch Abschnitt 12.6.2).
Falls Ihnen die Tragweite dieses Dialogs noch nicht ganz klar sein sollte: Wenn Sie Common Language Runtime Exceptions anklicken und eine der Optionen verändern, gilt diese Einstellung für alle vordefinierten .NET-Fehler. Wenn Sie angeben, dass sofort beim Auftreten eines Fehlers der Debugger gestartet werden soll (wie dies in Abbildung 11.8 für System.ArgumentExceptions der Fall ist), dann erfolgt immer, wenn dieser Fehler auftritt, sofort eine Programmunterbrechung. Das gilt auch dann, wenn der Fehler eigentlich abgesichert wäre; und auch dann, wenn der Fehler in einer .NET-Bibliothek (und nicht in Ihrem Programm) auftritt!
TIPP
11.2 Fehlersuche (Debugging)
497
Manchmal tritt ein Fehler nicht unmittelbar in Ihrem Code auf, sondern in einer .NET-Bibliothek. (Es kann trotzdem sein, dass Sie an dem Fehler direkt oder indirekt schuld sind, weil Sie einen falschen Parameter übergeben haben, ein noch benötigtes Objekt gelöscht haben etc.) Derartige Fehler sind sehr schwer zu lokalisieren. Versuchen Sie in solchen Fällen, einfach beim Auftreten aller Fehler sofort in den Debugger zu springen. Dazu markieren Sie Common Language Runtime Exceptions und aktivieren die erste Option. Manchmal gelingt es mit dieser Radikalmaßnahmen die Ursache des Problems zu erkennen.
Abbildung 11.8: Einstellung der Reaktion auf Fehler
11.2.3 Debugging-Anweisungen im Code Sie können in Ihren Programmcode verschiedene Anweisungen einbauen, die bei der Fehlersuche helfen: •
Stop unterbricht die Programmausführung und führt zurück in die Entwicklungsumgebung. Dort können Sie das Programm mit DEBUGGEN|WEITER fortsetzen.
498
11 Fehlersuche und Fehlerabsicherung
Stop hat eine ähnliche Wirkung wie ein Haltepunkt, lässt sich aber bisweilen einfacher steuern, etwa in Kombination mit If-Abfragen: If i = 5 Then Stop.
Bei Release-Kompilaten führt Stop ebenfalls zu einer Unterbrechung. Der Anwender kann an dieser Stelle entweder einen Debugger starten oder das Programm fortsetzen (Button NEIN). •
Die Debug-Klasse stellt eine Reihe von Eigenschaften und Methoden zur Verfügung, die bei der Anzeige von Debugging-Informationen in der Entwicklungsumgebung hilfreich sind. Die Ausgaben können während oder nach der Programmausführung im Ausgabefenster angesehen werden (ANSICHT|ANDERE FENSTER|AUSGABEFENSTER). Bei Release-Kompilaten werden Debug-Methoden ignoriert.
•
Zwischen den Zeilen #If Debug Then ... und #End Debug können Sie Code einbauen, der nur dann ausgeführt wird, wenn das Programm als Debug-Kompilat erstellt wird (siehe auch Abschnitt 1.3.5).
Debug-Ausgaben
HINWEIS
Mit den Methoden Debug.Write und WriteLine können Sie Informationen über den Zustand Ihres Programms in das Ausgabefenster schreiben. Als Parameter kann entweder eine Zeichenkette oder ein Objekt übergeben werden, bei dem dann die ToString-Methode ausgewertet wird. Die Methoden WriteIf und WriteLineIf funktionieren wie Write[Line], allerdings erfolgt die Ausgabe nur, wenn die im ersten Parameter angegebene Bedingung erfüllt ist. Debug.Write[Line] ist im Gegensatz zu Console.Write[Line] leider nicht in der Lage, Formatanweisungen zu interpretieren. Debug.WriteLine("abc = {0}", abc) führt also nicht zu der von Console.WriteLine vertrauten Ausgabe. Sie müssen die Ausgabe stattdessen in der Form Debug.WriteLine("abc = " + abc.ToString()) durchführen.
Die Methode Indent bewirkt, dass alle weiteren Textausgaben eingerückt werden. Wenn Indent mehrfach ausgeführt wird, erhöht sich das Außmaß der Einrückung. Unindent reduziert das Maß der Einrückung. IndentLevel gibt an, um wie viele Stufen (Ebenen) Text momentan eingerückt wird. IndentSize gibt an, um wie viele Zeichen der Text pro IndentEbene eingerückt wird (per Default 4).
Debug-Ausgaben umleiten Per Default erfolgen die Debug-Ausgaben nur in das Ausgabefenster der Entwicklungsumgebung. Mit Listener.Add können Sie weitere TextWriterTraceListener-Objekte angeben. An die New-Methode dieser Klasse müssen Sie ein IO.Stream-Objekt übergeben. Auf diese Weise können Sie die Debug-Ausgaben beispielsweise auch im Konsolenfenster (Console.Out) anzeigen oder in einer Datei speichern: Dim fname As String = IO.Path.GetTempFileName() Debug.Listeners.Add(New TextWriterTraceListener(Console.Out))
11.2 Fehlersuche (Debugging)
499
Debug.Listeners.Add( _ New TextWriterTraceListener(New IO.StreamWriter(fname)))
Assertion Mit der Methode Assert (to assert: behaupten, erklären, Anspruch oder Recht geltend machen) können Sie eine beliebige Bedingung testen. Debug.Assert(Not IsNothing(parameter), "fehlermeldung")
Wenn die Bedingung nicht (!) erfüllt ist, wird die in Abbildung 11.9 dargestellte Nachrichtenbox angezeigt. Mit den drei überdurchschnittlich unsinnig beschrifteten Buttons können Sie das Programm anschließend beenden, debuggen oder fortsetzen. Gleichzeitig wird eine Information über den festgestellten Fehlerzustand ins Ausgabefenster bzw. in die entsprechende Datei geschrieben. Assert-Anweisungen eignen sich beispielsweise dazu, um die Parameter von Prozeduren oder Methoden zu überprüfen oder um während der Programmentwicklung andere unzulässige Zustände festzustellen. Assert-Fehlermeldungen sind keine Exceptions und können daher nicht durch Catch-Try abgefangen werden. Assert-Anweisungen werden in ReleaseKompilaten ignoriert.
Abbildung 11.9: Assertion-Nachricht
Mit der Methode Fail können Sie eine Assertion-Fehlermeldung ausgeben. Im Unterschied zu Assert gibt es bei Fail keinen Parameter für die Bedingung, so dass die Fehlermeldung immer angezeigt wird.
Syntaxzusammenfassung Debug-Klasse – Methoden und Eigenschaften Assert(bedingung [, text])
zeigt eine Assertion-Fehlermeldung an, wenn die Bedingung nicht (!) erfüllt ist. Anschließend kann das Programm beendet oder fortgesetzt werden.
Close()
schließt Dateien für Debug-Ausgaben.
Fail(text)
zeigt eine Assertion-Fehlermeldung an.
500
11 Fehlersuche und Fehlerabsicherung
Debug-Klasse – Methoden und Eigenschaften Flush()
speichert noch gepufferte Ausgaben in den Ausgabedateien.
Indent()
rückt alle weitere Ausgaben um eine Ebene ein.
IndentLevel
gibt an, um wie viele Ebenen Ausgaben eingerückt werden.
IndentSize
gibt an, um wie viele Zeichen Ausgaben pro Ebene eingerückt werden.
Listeners
verweist auf die Liste aller TraceListener-Objekte, an die Fehlermeldungen geschrieben werden (per Default nur das Ausgabefenster).
Unindent()
reduziert die Einrückung für weitere Ausgaben.
Write[Line](text)
schreibt den Text in das Ausgabefenster.
Write[Line]If(bedingung, text)
schreibt den Text in das Ausgabefenster, wenn die angegebene Bedingung erfüllt ist.
11.2.4 Fehlersuche in Windows.Forms-Programmen
HINWEIS
Bei Windows-Programmen kann es vorkommen, dass beim Auftreten eines Fehlers weder die tatsächliche Fehlerursache angegeben noch die Zeile gekennzeichnet wird, die den Fehler verursacht hat. Noch schlimmer: In seltenen Fällen kommt es trotz eines Fehlers zu gar keiner Fehlermeldung; die Ausführung der Prozedur wird einfach abgebrochen. In so einem Fall ist es schon recht schwierig, auch nur festzustellen, dass überhaupt ein Fehler vorliegt (geschweige denn, diesen auch zu finden). Ich hatte mehrfach das Problem, dass sich einzelne Debugging-Fenster einfach nicht öffnen ließen. Grundsätzlich gilt, dass viele Fenster erst dann geöffnet werden, wenn gerade ein Programm unterbrochen ist. Wenn diese Voraussetzung erfüllt ist und es dennoch nicht klappt, kann es sein, dass die Entwicklungsumgebung mit der Fensteranordnung so durcheinander gekommen ist, dass das Fenster nicht geöffnet werden kann. Abhilfe brachte der Button FENSTERLAYOUT ZURÜCKSETZEN im ersten Dialogblatt von EXTRAS|OPTIONEN. (Damit verlieren Sie alle eigenen Einstellungen der Entwicklungsumgebung, die die Fensteranordnung betreffen.)
Beispiel 1 Ein Windows-Programm mit einem PictureBox-Steuerelement enthält die folgende (fehlerhafte) Ereignisprozedur PictureBox1_Paint:
11.2 Fehlersuche (Debugging)
501
' Beispiel fehler\gdi-debugging Public Class Form1 Inherits System.Windows.Forms.Form [ ... Vom Windows Form Designer generierter Code ...] Private Sub PictureBox1_Paint(ByVal sender As Object, _ ByVal e As System.Windows.Forms.PaintEventArgs) _ Handles PictureBox1.Paint Dim gr As Graphics = e.Graphics gr.DrawLine(Pens.Red, 0, 0, 100, 100) gr.Dispose() 'Achtung, Fehler! End Sub End Class
Wenn das Programm ausgeführt wird, tritt in der ersten Zeile (Public Class Form1) der Fehler System.ArgumentException auf. Was ist passiert? Das Problem ist die Zeile gr.Dispose, mit dem das aus dem e-Parameter stammende Graphics-Objekt gelöscht wird. Das ist nicht erlaubt, weil Sie das Objekt nicht selbst erzeugt haben und es intern offensichtlich noch benötigt wird. Der Fehler tritt aber erst nach dem Ende der Ereignisprozedur im internen Code der .NET-Klassen auf und kann daher nicht lokalisiert werden. Es scheint keine Möglichkeit zu geben, derartige Fehler gezielt zu suchen. Beim vorliegenden Beispiel fällt der Verdacht natürlich sofort auf PictureBox1_Paint (weil das die einzige Ereignisprozedur des Programms ist), bei größeren Anwendungen müssen Sie sich aber wohl auf Ihre Intuition verlassen. Wenn Sie in PictureBox1_Paint einen Haltepunkt setzen und das Programm Zeile für Zeile ausführen, werden Sie bemerken, dass der Fehler unmittelbar nach dem Aufruf der Dispose-Methode auftritt.
Beispiel 2 Das zweite Beispiel entstand beim Versuch, Drag&Drop für Listenfelder zu realisieren (siehe auch Abschnitt 15.12.4). Um den Fehler nachzuvollziehen, klicken Sie im rechten Listenfeld einen Wochentag an und verschieben den Eintrag – ohne die Maustaste dazwischen loszulassen! – in das linke Listenfeld mit den Farben. Es passiert nichts, d.h., der Wochentag wird nicht in das andere Listenfeld verschoben. Es tritt aber auch keine Fehlermeldung auf. Beenden Sie das Programm, führen Sie DEBUGGEN|AUSNAHMEN aus und aktivieren Sie für alle Ausnahmen die Option WENN DIE AUSNAHME AUSGELÖST WIRD|IN DEN DEBUGGER SPRINGEN. Nun führen Sie exakt den gleichen Vorgang aus wie zuvor. Diesmal tritt in der Zeile n = ListBox2.SelectedIndices(i) der Fehler IndexOutOfRangeException auf. Wie ein Blick in das Aufruffenster beweist (siehe Abbildung 11.11), ist dieser Fehler nicht im VB-Code aufgetreten, sondern in der Windows.Forms-Bibliothek bei der Ausführung einer internen GetEntryObject-Methode.
502
11 Fehlersuche und Fehlerabsicherung
Mit anderen Worten: An diesem Fehler sind nicht Sie schuld, sondern die Programmierer der .NET-Bibliotheken. (Der Ereignisfluss bei dieser Drag&Drop-Operation bringt offensichtlich die interne Verwaltung der ausgewählten Elemente des Listenfelds durcheinander. Es kann natürlich sein, dass dieser Fehler bei künftigen .NET-Versionen behoben wird und sich dann nicht mehr reproduzieren lässt.)
Abbildung 11.10: Drag&Drop-Beispielprogramm
' Beispiel fehler/drag-and-drop-debugging Private Sub ListBox1_DragDrop(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DragEventArgs) _ Handles ListBox1.DragDrop Dim i, n As Integer If e.Data.GetDataPresent(ListBox1.GetType()) Then ' ausgewählte Einträge kopieren und löschen If ListBox2.SelectedIndices.Count > 0 Then For i = ListBox2.SelectedIndices.Count - 1 To 0 Step -1 n = ListBox2.SelectedIndices(i) 'Fehler! ListBox1.Items.Add(ListBox2.Items(n)) ListBox2.Items.Remove(ListBox2.Items(n)) Next End If End If End Sub
11.2 Fehlersuche (Debugging)
Abbildung 11.11: Die Aufrufliste beim Auftreten des Fehlers
503
12 Spezialthemen Dieses Kapitel gibt eine Einführung in eine Reihe von Spezialthemen bzw. in etwas exotischere Klassen der .NET-Bibliothek. Ob Sie Informationen über das Betriebssystem bzw. seine Umgebungsvariablen ermitteln, API-Funktionen aufrufen, eigene Programme durch Multithreading effizienter strukturieren oder fremde Programme starten oder per Automation steuern möchten – dieses Kapitel hilft Ihnen bei den ersten Schritten. 12.1 12.2 12.3 12.4 12.5 12.6 12.7
Ein- und Ausgabeumleitung (Konsolenanwendungen) Systeminformationen ermitteln Sicherheit Externe Programme starten Externe Programme steuern (Automation) Multithreading API-Funktionen verwenden (Declare)
506 507 514 516 519 528 546
506
12.1
12 Spezialthemen
Ein- und Ausgabeumleitung (Konsolenanwendungen)
Zirka zwei Drittel aller Beispielprogramme dieses Buchs sind Konsolenanwendungen. Der Grund dafür ist einfach: Die Struktur derartiger Programme ist leicht zu verstehen, der Overhead zur Demonstration eines einfachen Effekts sehr gering. Die Grundprinzipien von Konsolenanwendungen und die beiden Methoden Console.Write[Line] und .Read[Line] wurden in Abschnitt 1.1 bereits einleitend vorgestellt.
Standardein- und ausgabe An dieser Stelle geht es um eine seltener genutze Funktion der Klasse Console (Namensraum System, Bibliothek mscorlib.dll). Wie in C-Programmen schon seit rund 20 Jahren, können Sie nun endlich auch in VB.NET auf die Standardein- und ausgabekanäle zugreifen. Das ermöglicht die Entwicklung von Konsolenprogrammen, die wie unter Unix das Ergebnis eines anderen Programms verarbeiten. Falls Ihnen die Möglichkeiten der Standardeingabe bzw. -ausgabe unbekannt sind, sollten Sie zuerst einige Experimente in einem Eingabeaufforderungsfenster durchführen: dir /b
zeigt eine Liste aller Dateien im aktuellen Verzeichnis an. (Die Option /b bewirkt, dass dir nur die Namen angibt, nicht aber andere Zusatzinformationen.) dir /b > datei.txt
speichert diese Liste in der Datei datei.txt. Das Zeichen > bewirkt, dass die Standardausgabe vom Konsolenfenster in eine Datei umgeleitet wird. dir /b | sort /r > datei.txt
erzeugt ebenfalls eine Liste aller Dateien. Die Standardausgabe wird durch das Zeichen | an das Programm sort weitergegeben. sort sortiert die Dateien wegen der Option /r in umgekehrter Reihenfolge und schreibt das Ergebnis an den Standardausgabekanal. Von dort werden Sie wegen > wieder in eine Datei umgeleitet.
Standardein- und ausgabe in Konsoleanwendungen In Konsolenanwendungen liest Read[Line] Text aus dem Standardeingabekanal, während Write[Line] Text an den Standardausgabekanal schreibt. Darüber hinaus bieten Console.In bzw. .Out einen direkten Zugriff auf die zugrunde liegenden TextReader- bzw. TextWriterObjekte der Standardkanäle. (Die in Abschnitt 10.5 beschriebenen Eigenschaften und Methoden von StreamReader und StreamWriter können daher auch auf In bzw. Out angewendet werden). Mit SetIn bzw. SetOut können Sie die Standardkanäle woandershin umleiten, so dass beispielsweise alle Eingaben des Programms nicht mehr interaktiv erwartet werden, sondern aus einer Datei gelesen werden.
12.2 Systeminformationen ermitteln
507
Das folgende Beispiel zeigt ein Konsolenprogramm, das zeilenweise Eingaben aus dem Standardeingabekanal liest und diese Zeichenketten dann in umgekehrter Reihenfolge wieder ausgibt. (Dieses Programm würde auch funktionieren, wenn Sie auf In und Out verzichten und die gewöhnlichen Read- bzw. WriteLine-Methoden der Console-Klasse verwenden. Dank In und Out stehen Ihnen noch eine Menge zusätzlicher Programmiermöglichkeiten zur Verfügung, die in diesem sehr einfachen Beispiel aber nicht benötigt wurden.) ' Beispiel spezial\reversetext Sub Main() Dim line As String Do line = Console.In.ReadLine() If IsNothing(line) Then Exit Do Console.Out.WriteLine(StrReverse(line)) Loop End Sub
Um das Programm auszuprobieren, kompilieren Sie es, öffnen ein Eingabeaufforderungsfenster, wechseln in das bin-Verzeichnis des Programms und führen dann die in Abbildung 12.1 dargestellten Kommandos aus.
Abbildung 12.1: Ein- und Ausgabeumleitung mit Konsolenanwendungen
12.2
Systeminformationen ermitteln
Dieser Abschnitt stellt einige Eigenschaften und Methoden der Klassen System.Environment und Microsoft.VisualBasic.Interaction vor und gibt eine erste Einführung in die Bibliothek System.Management. Diese Klassen sind dabei behilflich, diverse system- und programmspezifische Informationen zu ermitteln: die Kommandozeile des laufenden Programms, die Umgebungsvariablen des Betriebssystems, die Pfade spezieller Verzeichnisse (z.B. des Windows-Systemverzeichnisses), den Rechnernamen, den aktuellen Benutzernamen, die Liste der an den Rechner angeschlossenen Laufwerke etc.
VERWEIS
508
12 Spezialthemen
Systeminformationen, die spezifisch für die Windows-Programmierung sind (also z.B. die Anzahl der angeschlossenen Monitore, deren Auflösung etc.) können mit den Windows.Forms-Klassen SystemInformation und Screen ermittelt werden. Diese Klassen werden in Abschnitt 15.2.8 kurz vorgestellt.
12.2.1 System.Environment-Klasse Die Klasse Environment (Namensraum System, Bibliothek mscorlib.dll) gibt Zugriff auf zahlreiche Basiseinstellungen des Betriebssystems bzw. der Umgebung, in der das aktuelle Programm ausgeführt wird. Alle Methoden und Eigenschaften können verwendet werden, ohne vorher ein Objekt dieser Klasse zu erzeugen. (Ein Teil der Informationen kann alternativ auch mit den Methoden der Klasse Microsoft.VisualBasic.Interaction ermittelt werden. Der Vorteil besteht im geringeren Tippaufwand.)
Umgebungsvariablen ermitteln Durch das Betriebssystem sind eine Reihe so genannter Umgebungsvariablen (environment variables) definiert. Diese Variablen geben Auskunft über das laufende Betriebssystem, über die Anzahl der Prozessoren, über einige wichtige Verzeichnisse, über Pfade zu Programmen, über Pfade zu verschiedenen Bibliotheken etc. Bei der Installation von Programmen werden dem System manchmal neue Umgebungsvariablen hinzugefügt. Sie können sich diese Variablen in der Systemsteuerung ansehen (SYSTEM|ERWEITERT|UMGEBUNGSVARIABLEN). Unter VB.NET können Sie Systemvariablen wahlweise mit den Methode Environ oder Environment.GetEnvironmentVariable auslesen. Die folgenden Zeilen liefern jeweils den Pfad zum Windows-Verzeichnis. Dim s As String s = Environ("windir") s = Environment.GetEnvironmentVariable("windir")
Eine Aufzählung aller Umgebungsvariablen erhalten Sie mit GetEnvironmentVariables. Die Methode liefert ein Collections.IDictionary-Objekt zurück. Die folgende Schleife zeigt die Auswertung dieser Aufzählung. Dim entry As DictionaryEntry For Each entry In Environment.GetEnvironmentVariables Console.WriteLine("{0} = {1}", _ entry.Key.ToString, entry.Value.ToString) Next
12.2 Systeminformationen ermitteln
509
Kommandozeile auswerten Wenn Ihr Programm mit START|AUSFÜHREN oder aus einem Eingabeaufforderungsfenster ('DOS-Fenster') heraus gestartet wird, können Parameter an das Programm übergeben werden. Diese Parameter werden als Kommandozeile bezeichnet. Dasselbe gilt auch für Programme, die durch einen Doppelklick auf ein damit verbundenes Dokument aus dem Windows Explorer starten. (Wenn Sie also eine *.doc-Datei per Doppelklick öffnen, wird winword.exe gestartet und der Dateiname in der Kommandozeile übergeben.) Diesen Mechanismus können Sie auch für eigene Programme nutzen, wenn Sie diese mit einem eigenen Dateityp verbinden. Ein entsprechendes Beispiel finden Sie in Abschnitt 18.4.5, wo es weniger um die einfache Auswertung der Kommandozeile geht, als vielmehr um die Registrierung eines neuen Dateityps bei der Installation des Programms. Den Inhalt der Kommandozeile können Sie wahlweise mit der Methode Command oder mit der Eigenschaft Environment.CommandLine ermitteln. Beachten Sie, dass CommandLine gleichsam als ersten Parameter den Namen des Programms enthält und die tatsächlichen Parameter erst im Anschluss daran folgen. Wenn Sie die Parameter einzeln auslesen möchten, liefert Environment.GetCommandLineArgs ein String-Feld; das erste Element dieses Felds ist wiederum der Name des aktuellen Programms, erst die folgenden Elemente sind die tatsächlichen Parameter.
VERWEIS
Spezielle Verzeichnisse ermitteln Mit der Methode Environment.CurrentDirectory können Sie das aktuelle Verzeichnis ermitteln. SystemDirectory liefert das Windows-Systemverzeichnis, GetFolderPath(..) liefert den Pfad zu weiteren speziellen Verzeichnissen. All diese Eigenschaften bzw. Methoden werden in Abschnitt 10.3.4 beschrieben.
Benutzername, Rechnername, OS-Version Die folgenden Eigenschaften der System.Environment-Klasse bedürfen keiner langen Erklärung, weswegen Sie hier nur Syntaxboxen mit einer Zusammenfassung der Eigenschaften finden. System.Environment-Klasse – Rechnerspezifische Informationen MachineName
liefert den Rechnernamen in einer Arbeitsgruppe (NetBIOS-Name).
NewLine
gibt an, welche Zeichen zur Zeilentrennung in Textdateien verwendet werden (unter Windows Chr(13)+Chr(10)).
OSVersion
liefert Informationen über das Betriebssystem (als System.OperatingSystem-Objekt).
510
12 Spezialthemen
System.Environment-Klasse – Rechnerspezifische Informationen TickCount
liefert die Anzahl der Ticks (100 ns), seit der Rechner gestartet wurde.
UserDomainName
liefert den Rechnernamen in einer Domain (stimmt meist mit HostName überein).
System.Environment-Klasse – loginspezifische Informationen UserName
Benutzername (Login-Name)
UserInteractive
gibt an, ob ein interaktiver Login vorliegt (True/False).
System.Environment-Klasse – programmspezifische Informationen Version
gibt Informationen über die Version des laufenden Programms an (als System.Version-Objekt).
WorkingSet
gibt die Menge des Speichers an, der dem Prozess zugeordnet ist (in Byte).
Betriebssystemversion ermitteln Abschließend noch ein Beispiel zur Ermittlung der Version des laufenden Betriebssystems. Environment.OSVersion liefert ein OperatingSystem-Objekt zurück. Dessen Eigenschaft Platform gibt den Betriebssystemtyp an (in Form eines Elements der PlatformID-Aufzählung). Die möglichen Werte sind im Kasten unten zusammengefasst. Die Eigenschaft Version des OSVersion-Objekts liefert ein System.Version-Objekt. Dessen Eigenschaften Major, Minor und Revision geben die Bestandteile der internen Versionsnummer des Betriebssystems an. (Wenn das Betriebssystem die Versionsnummer 5.3.7 hat, dann gilt Major=5, Minor=3 und Revision=7.) Build enthält eine weitere interne Versionsnummer. (Sie wird jedes Mal um eins erhöht, wenn Microsoft das gesamte Betriebssystem neu kompiliert.) Es gibt leider keine eigene Eigenschaft, die die Nummer des installierten Service-Packs angibt. Sie können grundsätzlich aus der Build-Nummer auf die SP-Nummer schließen, es scheint aber leider keine zentrale Tabelle zu geben, die die Zuordnung zwischen Betriebssystemversion, Build-Nummer und SP-Nummer angibt. Die folgenden Zeilen schreiben die wichtigsten Betriebssysteminfos in ein Konsolenfenster. Dim os As OperatingSystem = Environment.OSVersion Dim ver As Version = os.Version Console.WriteLine(os.Platform.ToString) Console.WriteLine( _ "Major: {0} Minor: {1} Revision: {2} Build: {3}", _ ver.Major, ver.Minor, ver.Revision, ver.Build)
12.2 Systeminformationen ermitteln
511
Bei Windows 2000 SP 2 liefert das Miniprogramm folgendes Ergebnis: Win32NT Major: 5
Minor: 0
Revision: 0 Build: 2195
System.OperatingSystem-Klasse Platform
gibt in Form von Aufzählungselementen den Betriebssystemtyp an. In Frage kommen zurzeit: PlatformID.Win32S: 16-Bit-Windows-Version mit 32-Bit-
Emulationsschicht PlatformID.Win32Windows: Windows 95, 98, ME PlatformID.Win32NT: Windows NT, 2000, XP Version
liefert ein System.Version-Objekt.
System.Version-Klasse Build
Build-Nummer (durchlaufende Nummer, je höher, desto aktueller)
Major
Hauptversionsnummer (3 für Windows NT 3.x, 4 für Windows 9x, ME und NT 4, 5 für Windows 2000, XP und .NET-Server)
Minor
Unterversionsnummer (0 für Windows 95, NT 4, 2000, 1 für Windows XP und .NET-Server, 10 für Windows 98, 51 für Windows NT 3.51 90 für Windows ME)
Revision
Revisionsnummer
12.2.2 System.Management-Bibliothek (WMI) Die System.Management-Bibliothek bietet im gleichnamigen Namensraum zahlreiche Klassen zur Systemverwaltung. Genau genommen helfen diese Klassen, die Funktionen der Windows Management Instrumentation (kurz WMI) zu nutzen. Damit können Sie verschiedene Performance-Parameter ermitteln (CPU-Auslastung etc.), Benutzer und Passwörter verwalten, verschiedene Server-Dienste steuern (SQL-Server, IIS etc.), die an den Rechner angeschlossenen Laufwerke ermitteln, Netzwerkparameter lesen und verändern etc. Der Platz reicht hier nicht aus, um auf die System.Management-Bibliothek ausführlich einzugehen. Das in diesem Abschnitt vorgestellte Beispiel soll Ihnen aber 'Lust machen, sich selbst ein wenig in das Thema einzulesen. Beachten Sie, dass Sie einen Verweis auf die Bibliothek einrichten müssen, bevor Sie deren Klassen verwenden können!
VERWEIS
512
12 Spezialthemen
Ausführliche Informationen zur Nutzung dieser Bibliothek finden Sie in der Hilfe: Suchen Sie einfach nach den Begriffen Verwalten Anwendungen Verwendung WMI. ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpconmanagingapplicationsusingwmi.htm
Eine gute Einführung in WMI und in die Verwendung der System.Management-Bibliothek gibt auch das .NET-Klassenhandbuch von Holger Schwichtenberg und Frank Eller (siehe Quellenverzeichnis im Anhang).
Beispiel – Informationen über die Laufwerke des Rechners ermitteln WMI-intern werden alle über den Computer verfügbaren Informationen in einer Art Datenbank verwaltet. Die Besonderheit besteht darin, dass Sie diese Informationen wie durch Datenbankabfragen auslesen können. Dazu benötigen Sie ein Objekt der Klasse ManagementObjectSearcher, um die Abfrage zu formulieren und mit Get auszuführen. Als Ergebnis erhalten Sie eine ManagementObjectCollection-Aufzählung, die eine Reihe von ManagementObject-Objekten enthält. Diese Objekte verweisen wiederum auf PropertyDataObjekte mit den eigentlichen Daten. Das folgende Beispielprogramm ermittelt alle am Rechner verfügbaren Laufwerke. Diese Informationen befinden sich in der WMI-Datenbank in einer Win32_LogicalDisk-Tabelle. Das Programm ermittelt alle Elemente dieser Tabelle und zeigt wiederum alle Eigenschaften der gefundenen Elemente in einem Konsolenfenster an (siehe Abbildung 12.2). Die TryCatch-Konstruktion ist erforderlich, weil manche Eigenschaften nicht mit ToString in Zeichenketten umgewandelt werden können. Diese Eigenschaften werden einfach übersprungen. Für die praktische Anwendung dieses Beispiels ist vor allem die DriveType-Eigenschaft interessant, die ohne die System.Management-Bibliothek leider nicht ermittelt werden kann. Laut der WMI-Dokumentation sind folgende Werte vorgesehen:
HINWEIS
0 1 2 3 4 5 6
unbekannt Datenträger ohne Wurzelverzeichnis (no root directory, was immer das bedeutet) Diskette (oder ein anderer, nicht fest angeschlossener Datenträger) lokale (Fest-)Platte Netzwerkverzeichnis CD-/DVD-Laufwerk RAM-Disk System.Management ist eine eigene Bibliothek und nicht Teil von System.dll oder mscorlib.dll! Deswegen müssen Sie die Bibliothek explizit mit PROJEKT|VERWEIS HINZUFÜGEN in Ihr Projekt aufnehmen, damit Sie die System.Management-Klassen nützen
können!
12.2 Systeminformationen ermitteln
Abbildung 12.2: Alle verfügbaren Informationen über die Laufwerke A:, C:, D: etc.
' Beispiel spezial\laufwerke ' dieses Programm setzt voraus, dass Sie einen Verweis ' auf die System.Management-Bibliothek einrichten Sub Main() Dim mos As Management.ManagementObjectSearcher Dim moc As Management.ManagementObjectCollection Dim mo As Management.ManagementObject Dim pd As Management.PropertyData mos = New Management.ManagementObjectSearcher( _ "SELECT * FROM Win32_LogicalDisk") moc = mos.Get() For Each mo In moc Console.WriteLine("------------") For Each pd In mo.Properties Try Console.WriteLine(pd.Name + " = " + pd.Value.ToString) Catch End Try Next Console.ReadLine() Next moc.Dispose() mos.Dispose() End Sub
513
514
12 Spezialthemen
12.3
Sicherheit
ACHTUNG
In Abschnitt 2.4 wurden die Grundkonzepte der .NET-Sicherheit kurz skizziert. Dieser Abschnitt greift das Thema nochmals auf und beschreibt, wann es in VB.NET-Programmen zu Problemen aufgrund mangelnder Zugriffsrechte kommen kann und wie sich mit diesen Problemen umgehen lässt. Ich habe bereits in Abschnitt 2.4 darauf hingewiesen, dass die vollständige Beschreibung der .NET-Sicherheitsmechanismen und ihrer Steuerung durch VB.NETCode zumindest ein ganzes Kapitel füllen könnte (wenn man noch auf Themen wie Krypthographie eingeht, auch ein ganzes Buch). An dieser Stelle fehlt dazu aber der Platz. Ich betone also ausdrücklich, dass das in diesem Buch vermittelte Wissen zum Thema Sicherheit bestenfalls ein Ausgangspunkt für eigene Experimente und zur weitergehenden Lektüre sein kann!
Einige Beispiele für sicherheitskritische Methoden bzw. Kommandos Sicherheitskritischen Methoden sind durchaus nicht immer ohne weiteres erkennbar. Die folgende Liste zählt einige Kommandos bzw. Methoden auf, die bei der Ausführung ohne ausreichende Rechte zu Fehlern (SecurityExceptions) führen: •
Mit dem aus VB6-Zeiten stammenden Kommando End wird das laufende Programm sofort beendet. Das Kommando wirkt vollkommen harmlos, aber es erfordert das Recht SecurityPermissionFlag.UnmanagedCode. Wenn dieses Recht nicht gegeben ist, führt End zu einem Fehler!
•
Mit New IO.DirectoryInfo("c:\").GetFiles() ermitteln Sie eine Liste aller Dateien, die sich auf der Festplatte C: im Wurzelverzeichnis befinden. Die Methode GetFiles darf allerdings nur ausgeführt werden, wenn Sie das Recht Security.Permissions.FileIOPermission besitzen. Ähnliche Probleme können bei allen System.IO-Methoden auftreten.
•
Der Aufruf von API-Funktionen und die Verwendung von COM-Bibliotheken bzw. die Steuerung von COM-Programmen (Automation) führt bei unzureichenden Rechten zu einer SecurityException.
•
Die Methode Graphics.FromHwnd zur Ermittlung des Windows-Handle eines Fensters führt bei unzureichenden Rechten ebenfalls zu einer SecurityException.
Die Liste ließe sich fast beliebig fortführen. Zum Teil sind die erforderlichen Rechte bei der Dokumentation der Klasse bzw. Methode beschrieben, aber leider ist das nicht immer der Fall. Es ist daher eine gute Idee, die Programmausführung mit eingeschränkten .NETRechten einfach auszuprobieren!
12.3 Sicherheit
515
SecurityExceptions durch Catch/Try abfangen Unzureichende Rechte lösen normalerweise eine Security.SecurityException aus. Derartige Fehler können durch Try – Catch abfangen werden. Dim fi() As IO.FileInfo Try fi = New IO.DirectoryInfo("c:\").GetFiles() Catch e As Security.SecurityException Console.WriteLine(e.Message) End Try
Zugriffsrechte testen Die Basisrechte werden durch die Klassen des Namensraums Security.Permissions verwaltet. Wenn Sie überprüfen möchten, ob Ihr Programm ein bestimmtes Recht hat, erzeugen Sie ein Permission-Objekt und führen dann die Methode Demand aus. Wenn es dabei zu einem Fehler kommt, wissen Sie, dass Sie das angeforderte Recht nicht haben. Die folgenden Zeilen testen, ob Dateien aus dem Verzeichnis C:\ gelesen werden dürfen. Dazu wird ein FileIOPermission-Objekt erzeugt, wobei die gewünschte Operation und der Pfadname als Parameter an New übergeben werden. Dim fp As New _ Security.Permissions.FileIOPermission( _ Security.Permissions.FileIOPermissionAccess.Read, "c:\") Try fp.Demand() Catch e As Security.SecurityException Console.WriteLine(e.Message) End Try
Programmausführung von Rechten abhängig machen Bei manchen Programmen ist es zwecklos, dass diese überhaupt gestartet werden können, wenn sie in der Folge aufgrund unzureichender Rechte ihre Aufgabe ohnedies nicht erfüllen können. Um einen Rechtetest komfortabel beim Programmstart durchzuführen, gibt es zu jeder Permission-Klasse eine PermissionAttribute-Variante. Das entsprechende Attribut muss als Assembly-Attribut im Programmcode angegeben werden. Die folgenden Zeilen testen nochmals, ob Dateien aus dem Verzeichnis C:\ gelesen werden dürfen. Beachten Sie, dass die Parameter des Attributs nicht ganz mit denen der FileIOPermission-Klasse übereinstimmen.
516
12 Spezialthemen
12.4
Externe Programme starten
Dieser Abschnitt beschreibt einige Möglichkeiten, ein anderes (externes) Programm zu starten und eventuell auch wieder zu beenden bzw. auf sein Ende zu warten. Dabei kommen vor allem Methoden aus zwei Klassen zur Anwendung: •
Die Interaction-Klasse aus dem Namensraum Microsoft.VisualBasic bietet einige grundlegende und schon aus VB6 bekannte Möglichkeiten, andere Programme zu starten.
•
Mit der Process-Klasse aus dem Namensraum System.Diagnostics können Sie darüber hinaus eine Menge Informationen über das aktuellen Programm ermitteln und neue Programme starten. (Jedes Programm wird von einem so genannten Prozess ausgeführt, der wiederum aus mehreren Teilprozessen (Threads) bestehen kann. Multithreading wird in Abschnitt 12.6 beschrieben.)
VERWEIS
Manche Programme können von VB.NET nicht nur gestartet, sondern auch gesteuert werden. Dabei kommt der Mechanismus Automation zur Anwendung, der im nächsten Abschnitt behandelt wird. Wenn Sie Automation einsetzen möchten, müssen Sie zum Programmstart Get- oder CreateObject verwenden.
TIPP
Der Namensraum System.Diagnostics ist Teil der Bibliothek System.dll, die jedem VB.NET-Programm zur Verfügung steht. Die Diagnostics-Klassen werden unter anderem auch von den Debugging-Komponenten der Entwicklungsumgebung genutzt.)
Auf der CD finden Sie im Verzeichnis spezial\start-program ein kleines Beispiel, das die unten beschriebenen Programmiertechniken demonstriert. Auf den Abdruck des Beispiels wurde aus Platzgründen verzichtet.
Programm starten Die Methode Shell der Interaction-Klasse startet ein anderes Programm. Als einziger Parameter muss der Programmname als Zeichenkette übergeben werden. Bei Programmen, die sich im Windows-Verzeichnis befinden, reicht der reine Name (z.B. "notepad.exe"). Dasselbe gilt für Programme, die sich in Verzeichnissen der Umgebungsvariablen PATH befinden. Wenn diese Voraussetzung nicht erfüllt ist, muss der vollständige Pfad angegeben werden (also z.B. "C:\Programme\Opera\Opera.exe"). Zusammen mit dem Programmnamen können auch Parameter angegeben werden. Beispielsweise startet das folgende Kommando den Texteditor und zeigt darin die Datei c:\readme.txt an. Dim processid As Integer processid = Shell("notepad.exe c:\readme.txt")
12.4 Externe Programme starten
517
Shell kennt drei optionale Parameter:
•
Style gibt an, wie das Programm geöffnet werden soll – beispielsweise in einem bildschirmfüllenden Fenster mit Eingabefokus (AppWinStyle.MaximizedFocus). Per Default gilt die Einstellung AppWinStyle.MinimizedFocus.
•
Wait gibt an, ob mit der Fortsetzung des VB.NET-Programms so lange gewartet werden soll, bis das aufgerufene Programm beendet wird (per Default False). Beachten Sie, dass
das VB.NET-Programm dadurch wirklich vollständig blockiert wird. Bei WindowsProgrammen funktioniert dann nicht einmal das Neuzeichnen des Fensters. •
Timeout gibt an, wie lange maximal auf das Programmende gewartet werden soll. Timeout wird nur berücksichtigt, wenn Wait:=True gilt. Die Zeitangabe erfolgt in Millisekun-
den. Die Defaulteinstellung lautet -1, d.h., es wird endlos gewartet. Wenn der Programmstart gelingt, liefert Shell als Ergebnis die Prozessnummer des Programms. Diese Nummer kann später z.B. dazu verwendet werden, um das Programm AppActivate in den Vordergrund zu bringen.
Laufendendes Programm aktivieren Mit der Methode AppActivate (Klasse Interactive) können Sie ein laufendes Programm bzw. Fenster aktivieren, wenn Sie entweder dessen Prozessnummer kennen (z.B. von einem früheren Shell-Aufruf) oder den Text, der in der Titelzeile des Fensters angezeigt wird. AppActivate endet mit einem ArgumentException-Fehler, wenn der Prozess nicht gefunden werden kann. Sie sollten den Aufruf daher durch Catch-Try absichern.
HINWEIS
AppActivate(processid)
Das durch AppActivate aktivierte Programm erhält zwar den Eingabefokus, seine Fensterposition wird aber nicht verändert. Wenn das Programm momentan verkleinert ist und nur als Icon in der Taskleiste angezeigt wird, wird es durch AppActivate nicht sichtbar.
Programm zum Anzeigen oder Bearbeiten einer Datei öffnen Mit der Methode Start (Klasse Diagnostics.Process) können Sie ein Dokument öffnen. Die Besonderheit von Start im Vergleich zu Shell besteht darin, dass als Parameter nur der Dateiname des Dokuments übergeben werden muss. Start ermittelt selbstständig das erforderliche Programm. Beispielsweise startet die folgende Zeile Adobe Acrobat und zeigt darin die angegebene PDF-Datei an. (Wenn der Dateiname wie hier ohne Pfad angegeben wird, wird die Datei im selben Verzeichnis gesucht, in dem sich die Programmdatei befindet.) Diagnostics.Process.Start("datei.pdf") Start akzeptiert als Parameter auch Web- und E-Mail-Adressen und öffnet dann den
Default-Webbrowser bzw. das Default-E-Mail-Programm.
518
12 Spezialthemen
Diagnostics.Process.Start("http://www.kofler.cc") Diagnostics.Process.Start("mailto:[email protected]") Start funktioniert natürlich nur dann, wenn der Datei ein Programm zugeordnet werden kann und dieses auch installiert ist. Wenn die Dateikennung dagegen unbekannt ist, löst Start einen Win32Exception-Fehler aus.
Wenn durch Start ein neues Programm gestartet wird, liefert die Methode als Ergebnis ein Process-Objekt zurück. Es kann aber auch vorkommen, dass das Programm schon vorher gelaufen ist (z.B. ein Webbrowser) und dass nur ein neues Fenster geöffnet wurde, um darin eine weitere Webseite anzuzeigen. In diesem Fall liefert Start als Ergebnis Nothing. (Das Ergebnis Nothing bedeutet also durchaus nicht, dass das Dokument nicht angezeigt wurde!) Sie können nun das Process-Objekt dazu verwenden, um Informationen über das laufende Programm zu ermitteln bzw. um dieses zu beenden. Die folgenden Zeilen bewirken, dass die Datei readme.txt nur fünf Sekunden lang angezeigt wird. Anschließend wird der Editor durch CloseMainWindow zum Programmende aufgefordert. (Wenn die Datei in der Zwischenzeit geändert wurde, erscheint dann ein Dialog zum Speichern, in dem der Anwender das Programm auch fortsetzen kann. Wenn der Anwender die Datei geschlossen hat, bevor CloseMainWindow aufgerufen wurde, kommt es zu einem Fehler.) Dim pr As Diagnostics.Process pr = Diagnostics.Process.Start("readme.txt") Threading.Thread.Sleep(5000) pr.CloseMainWindow()
Die folgenden Zeilen zeigen den Einsatz der Methode WaitForExit. Damit wartet das VB.NET-Programm, bis das gestartete Programm geschlossen wird.
VERWEIS
Dim pr As Diagnostics.Process pr = Diagnostics.Process.Start("readme.txt") pr.WaitForExit()
Weitere Beispiele für die Anwendung der Start-Methode finden Sie in Abschnitt 14.4.2 bei der Beschreibung des LinkLabel-Steuerelements.
VERWEIS
Programm durch SendKeys steuern Eine sehr primitive Form, ein anderes Programm zu steuern, bietet die Simulation von Tastatureingaben mit der Methode SendKeys. Eine derartige Steuerung ist aber von den Tastenkürzeln des Programms abhängig (und damit meist auch von der Sprache des Programms) und generell sehr fehleranfällig. Nähere Informationen zu SendKeys erhalten Sie in Abschnitt 15.9.
12.5 Externe Programme steuern (Automation)
12.5
519
Externe Programme steuern (Automation)
12.5.1 Grundlagen Der Begriff Automation bezeichnet die Möglichkeit, ein fremdes Programm zu steuern. Der Automation-Mechanismus basiert zwar auf COM-Technologie (alias ActiveX, alias OLE), kann aber auch mit VB.NET wegen dessen COM-Kompatibilität genutzt werden. Die Grundidee von Automation besteht darin, dass das VB.NET-Programm eine Verbindung zum Programm herstellt, das gesteuert werden soll. (Dieser Verbindungsaufbau kann z.B. auf der Basis einer vorhandenen Datei erfolgen.) Das Ergebnis ist ein Objekt, über dessen Methoden und Eigenschaften das externe Programm dann gesteuert werden kann. Wenn Ihnen das alles recht theoretisch erscheint, hier einige Beispiele: Dank Automation können Sie mit einem VB.NET-Programm Word starten, dort einen Text einfügen, formatieren und schließlich ausdrucken; Sie können eine Excel-Datei öffnen und daraus einige Zellen lesen, in Ihrem VB.NET-Programm verarbeiten, das Ergebnis wieder in einer anderen Zelle eintragen und die Excel-Datei dann speichern; Sie können Access starten und damit einen Datenbankbericht ausdrucken etc. Automation kann natürlich nur für solche Programme verwendet werden, die diesen Mechanismus unterstützen. Das ist unter anderem für alle aktuellen Office-Komponenten der Fall. Wenn Sie Programme weitergeben, die Automation nutzen, muss das zu steuernde Programm auch beim Anwender installiert sein (möglichst in derselben Version). Was in der Theorie toll klingt, führt in der Praxis leider zu schier endlosen Problemen. Obwohl Microsoft das Konzept von Automation schon 1994 mit Excel 5 realisierte, ist das Konzept nie richtig ausgereift: Automation ist abhängig von der am Rechner installierten Version des Programms, von dessen Sprache etc. Oft passiert es, dass nach dem Ende eines Programms, das Automation nutzt, eine Instanz des zu steuernden Programms (also z.B. Excel) weiterläuft und sich nur noch mit dem Task-Manager beenden lässt. Kurz und gut – es gibt 1000 Gründe, warum Automation in der Praxis selten so funktioniert, wie es eigentlich sollte. Eine Grundvoraussetzung für jede Automation-Anwendung besteht darin, dass Sie das Objektmodell des zu steuernden Programms kennen. Diese Hürde ist nicht zu unterschätzen. Beispielsweise kennt Excel ca. 150 Klassen mit weit über 1000 Methoden und Eigenschaften, deren Beschreibung ein ganzes Buch füllt. (Ich habe selbst ein derartiges Buch geschrieben und weiß, wovon ich spreche.) Zu diesen Problemen, die gar nichts mit .NET zu tun haben, kommen die grundsätzlichen Schwierigkeiten der .NET/COM-Kompatibilität hinzu, die in diesem Buch nicht einmal angedeutet werden. (Damit Sie sich eine Vorstellung von der Komplexität dieses Themas machen: die beiden bisher zu diesem Thema erschienenen englischen Bücher von Andrew Troelsen und Adam Nathan füllen zusammen 2500 Seiten!)
HINWEIS
12 Spezialthemen
Die obigen Absätze sollen Sie nicht davon abhalten, Automation selbst auszuprobieren, sondern sollen lediglich falsche bzw. überzogene Erwartungen verhindern. Mit etwas Experimentierfreude können Automation-Lösungen dann durchaus gelingen. (Senden Sie mir aber bitte keine E-Mails, wenn Sie auf Automation-Probleme stoßen! Ich kann in solchen Fällen nicht weiterhelfen und Ihnen insbesondere das Experimentieren nicht abnehmen.)
HINWEIS
520
In VB6 bestand die Möglichkeit, das zu steuernde Programm innerhalb des VB-Programms in einem OLE-Steuerelement sichtbar zu machen. Diese Möglichkeit gibt es in VB.NET nicht mehr.
Verwendung der Klassenbibliothek Das besondere Kennzeichen eines Programms, das sich durch Automation steuern lässt, besteht darin, dass es eine Klassenbibliothek zur Verfügung stellt (ähnlich wie die .NETBibliotheken, die Sie täglich nutzen). Diese Klassenbibliothek gibt Zugriff auf alle Objekte des Programms und ermöglicht somit die Steuerung. (Um es an einem Beispiel zu illustrieren: Bei Excel können Sie mit dem Worksheet-Objekt auf einzelne Tabellenblätter zugreifen, mit dem Range-Objekt auf Zellen oder Zellbereiche etc.) Damit Sie eine derartige Klassenbibliothek in Ihrem VB.NET-Programm nutzen können, müssen Sie einen Verweis auf die Bibliothek einrichten (im Projektmappenexplorer oder mit PROJEKT|VERWEISE). Sie finden die Klassenbibliothek unter den COM-Bibliotheken – die Bibliothek für Excel z.B. unter dem Namen MicrosoftExcel n.n Object Library. Über den Objektbrowser können Sie die Klassen dann ansehen (siehe Abbildung 12.3). VBA-Programmierern wird auffallen, dass viele Klassen, die in der VBA-Entwicklungsumgebung als Klassen dargestellt werden, von der .NET-Entwicklungsumgebung als Schnittstellen bezeichnet werden. Das hat aber keinen spürbaren Effekt auf die Programmierung. Nach der Auswahl der Bibliothek erstellt die Entwicklungsumgebung eine so genannte Wrapper-Bibliothek (z.B. Interop.Excel), die die Schnittstelle zwischen .NET und COM bildet. Bei Programmen wie Excel mit einer ziemlich großen Klassenbibliothek dauert die Erzeugung der Wrapper-Bibliothek ziemlich lang. Das wäre an sich noch kein Problem, wenn die resultierenden Bibliotheken dann vollständig wären. Das ist aber leider nicht immer der Fall. Beispielsweise fehlt nach dem Import der Excel-9-Bibliothek (entspricht Excel 2000) ein Großteil der rund 150 Klassen, was die weitere Programmierung fast unmöglich macht. Experimente mit der Excel-5-Bibliothek waren auch nicht erfolgreicher. (Diese Bibliothek ist kompatibel zum Objektmodell von Excel 5 und wird aus Kompatibilitätsgründen ebenfalls mit Excel 2000 mitgeliefert.) Diesmal klappte zwar der Import der meisten Klassen, dafür fehlten aber alle Konstanten. Darüber hinaus liefern viele Eigenschaften oder Methoden der Wrapper-Bibliothek als Ergebnis Object, obwohl sie im Original (also unter Excel) ganz andere Typen liefern.
12.5 Externe Programme steuern (Automation)
521
VERWEIS
Tests mit der Klassenbibliothek von Word 2000 führten zu keinen offensichtlichen Problemen. (Allerdings kenne ich die Word-Klassenbibliothek weniger gut als die von Excel. Es ist also nicht auszuschließen, dass ich diverse Word-Probleme übersehen habe.) Office XP alias Office 2002 stand mir für meine Tests leider nicht zur Verfügung, so dass ich auch dazu nichts sagen kann. Microsoft plant, die vollkommen unbefriedigende Import-Funktion für COM-Bibliotheken zumindest für seine eigenen Office-Programme dadurch zu lösen, dass es so genannte primary interop assemblies (PIAs) zur Verfügung stellt. Dabei handelt es sich um vorgefertigte Wrapper-Bibliotheken, die als offizielle Schnittstelle verwendet werden sollen, um Konflikte zwischen verschiedenen selbst erstellten WrapperBibliotheken zu vermeiden. Es ist zu hoffen, dass diese offiziellen Wrapper-Bibliotheken dann auch komplett sind. Bis zum Abgabetermin für dieses Buch habe ich leider keine derartigen PIAs für Office-Komponenten gefunden. Weitere Informationen finden Sie hier: http://msdn.microsoft.com/library/en-us/dndotnet/html/whypriinterop.asp
Abbildung 12.3: Einige Klassen zur Word-Automation
Late binding Wenn der Import der Klassenbibliothek wie bei Excel 2000 gänzlich scheitert, müssen Sie eben ohne einen Verweis auf diese Bibliothek arbeiten und late binding verwenden. late binding bedeutet, dass Sie Variablen des Typs Object verwenden und darauf Methoden und Eigenschaften anwenden. Der Compiler kann aber nicht überprüfen, ob es diese Methoden oder Eigenschaften überhaupt gibt. Deswegen lässt er diese Frage offen. Die Verbindung
522
12 Spezialthemen
zwischen Objekt und Methode oder Eigenschaft wird erst dann hergestellt, wenn das Programm ausgeführt wird – daher die Bezeichnung late binding. (Das Gegenteil von late binding wird übrigens als early binding bezeichnet und ist unter VB.NET der Normalfall.) late binding hat zwei wesentliche Nachteile: Erstens ist die Codeentwicklung sehr mühsam, weil die IntelliSense-Funktion nicht funktioniert und Sie ständig selbst im Objektkatalog nachsehen müssen, welche Klasse welche Eigenschaften oder Methoden kennt, welche Parameter und Rückgabewerte es hierfür gibt etc. Zweitens kann der Compiler die Korrektheit des Codes nicht überprüfen. Tippfehler (z.B. ein falscher Methodenname) treten daher erst bei der Ausführung des Programms auf.
TIPP
Um diese Nachteile zu umgehen, ist es am besten, den Code zuerst in einer anderen Programmiersprache (z.B. in VB6 oder in der VBA-Entwicklungsumgebung) zu entwickeln und den fertigen Code dann in das VB.NET-Programm einzufügen. Ganz ohne Änderungen ist das aber meist auch nicht möglich, weil die Namen von Klassen, Konstanten etc. bisweilen unter VB.NET anders sind. VB.NET unterstützt late binding nur dann, wenn Option Strict Off gilt! Mit Option Strict On kann ausschließlich early binding verwendet werden.
Verbindung zum Programm herstellen Es gibt zwei Möglichkeiten, um eine Verbindung zum Programm herzustellen, das gesteuert werden soll: •
Mit CreateObject("bibliothek.objekt") erzeugen Sie ein neues Objekt, z.B. ein WordDokument ("word.document") oder eine Excel-Tabelle ("excel.sheet"). Falls das Programm noch nicht läuft, wird es dazu gestartet. Dim doc As Object 'late binding doc = CreateObject("Word.Document") Dim doc As Word.Document 'early binding doc = CType(CreateObject("Word.Document"), Word.Document)
•
Mit GetObject(dateiname) öffnen Sie eine Datei und erhalten ebenfalls ein Objekt. GetObject startet bei Bedarf automatisch das richtige Programm (also z.B. Word, um die Datei "beispiel.doc" zu öffnen. Dim wb As Object 'late binding wb = CreateObject("C:\test\excel.doc") Dim wb As Excel.Workbook 'early binding wb = CType(CreateObject("C:\test\excel.doc"), Excel.Workbook)
12.5 Externe Programme steuern (Automation)
523
Die Methoden Create- und GetObject sind in der Interaction-Klasse von Microsoft.VisualBasic definiert. Welcher Klasse die zurückgegebenen Objekte angehören, hängt von der Klassenbibliothek des Programms ab. Welche Zeichenketten an CreateObject übergeben werden dürfen, hängt davon ab, wie die Programme in der Registrierdatenbank registriert sind. (Die Tabelle mit allen zulässigen Objektnamen können Sie mit regedit.exe ermitteln: HKEY_LOCAL_Machine|Software|Classes.) Durch Get- oder CreateObject gestartete Programme sind normalerweise unsichtbar. Die Microsoft-Office-Komponenten können über objekt.Application.Visible=True sichtbar gemacht werden. Bei Programmen anderer Hersteller kann es andere Mechanismen geben.
Verbindung zum Programm trennen Bisweilen komplizierter als der Verbindungsaufbau ist es, sich von dem zu steuernden Programm wieder zu trennen, wenn es nicht mehr benötigt wird. Wenn Sie nicht aufpassen, läuft das (womöglich unsichtbare) Programm weiter. Um das Programm explizit zu beenden, führen Sie üblicherweise object.Application.Quit() aus. Das Programm wird dadurch aber keineswegs tatsächlich sofort beendet. Es läuft vielmehr (mindestens) so lange weiter, solange es in ihrem VB.NET-Programm noch Verweise auf irgendein Objekt des Programms gibt. Wie lange es derartige Verweise gibt, hängt davon ab, wann diese durch eine garbage collection aus dem Speicher entfernt werden. Wenn Sie möchten, dass das externe Programm möglichst schnell beendet wird, dann sollten Sie folgenden Weg einschlagen: Achten Sie zum einen darauf, dass alle Variablen, die auf Objekte des Programms verweisen, entweder nicht mehr gültig sind (weil sie in einer Prozedur deklariert sind, die nicht mehr läuft) oder explizit auf Nothing gesetzt werden (also doc = Nothing). Lösen Sie zum anderen die garbage collection explizit zweimal hintereinander aus. (Fragen Sie mich nicht, warum gerade zweimal. Der Tipp stammt aus einem News-Gruppenbeitrag und hat sich in der Praxis bewährt.)
HINWEIS
doc.Application.Quit() doc = Nothing GC.Collect() GC.WaitForPendingFinalizers() GC.Collect() GC.WaitForPendingFinalizers()
'Programm zum Ende auffordern 'Objektvariablen löschen 'garbage collection auslösen 'auf das Ende der gc warten 'garbage collection nochmals auslösen 'auf das Ende der gc warten
Es kann sein, dass Word, Excel etc. schon läuft, wenn Sie Get- oder CreateObject ausführen. Dann wird das neue Objekt in der schon laufenden Instanz erzeugt. In diesem Fall sollten Sie das Programm nicht durch Quit beenden. Das Problem besteht allerdings darin, dass es in VB.NET nicht ohne weiteres zu erkennen ist, ob das Programm schon läuft. Daher sollten Sie vor der Ausführung von Quit überprüfen, ob im Programm noch andere Dokumente geöffnet sind. (In Excel können Sie das mit Application.Workbooks.Count tun.)
524
12 Spezialthemen
VERWEIS
Weitere Informationen In der Online-Hilfe wird das Thema Automation weitgehend ignoriert. Dafür gibt es aber gute Beiträge in der Knowledge-Base. Die folgende Aufzählung zählt drei einführende Artikel auf, eine Menge weitere finden Sie, wenn Sie im KnowledgeBase-Suchformular nach automate .NET oder automation .NET suchen. http://support.microsoft.com/default.aspx?scid=kb;en-us;Q301982 (VB.NET + Excel) http://support.microsoft.com/default.aspx?scid=kb;en-us;Q302814 (Excel-Ereignisse empfangen) http://support.microsoft.com/default.aspx?scid=kb;en-us;Q301656 (VB.NET + Word)
12.5.2 Beispiel – Daten aus einer Excel-Datei lesen Wegen der oben schon beschriebenen Probleme beim Versuch, die Excel-Klassenbibliothek zu importieren, verwendet das Beispielprogramm late binding. Das Programm öffnet die Datei sample.xls im Verzeichnis spezial\automation-excel, liest die Zellen [A1] bis [C3] aus und zeigt die Werte im Konsolenfenster an, trägt in [A4] die aktuelle Zeit ein und schließt die Datei dann wieder. Falls danach keine weitere Excel-Datei geöffnet ist (Workbooks.Count = 0), wird Excel durch Quit beendet. (Die Abfrage ist deswegen wichtig, weil Excel ja möglicherweise schon vor dem Programm gestartet wurde und dann nicht willkürlich beendet werden soll.) Excel endet allerdings erst nach zwei garbage collections, die alle Verweise auf das Excel-Objekt aus dem Speicher räumen.
Abbildung 12.4: Excel-Daten auslesen
12.5 Externe Programme steuern (Automation)
' Beispiel spezial\automation-excel Option Strict Off Sub Main() ' Excel-Datei bearbeiten process_xl_file() ' Excel beenden GC.Collect() 'garbage GC.WaitForPendingFinalizers() 'auf das GC.Collect() 'garbage GC.WaitForPendingFinalizers() 'auf das ' Programmende Console.WriteLine("Return drücken") Console.ReadLine() End Sub
525
collection auslösen Ende der gc warten collection nochmals auslösen Ende der gc warten
Sub process_xl_file() Dim i, j As Integer Dim xl, wb, ws As Object Dim fname As String fname = IO.Path.Combine(Environment.CurrentDirectory, _ "..\sample.xls") wb = GetObject(fname) xl = wb.Application ' xl.Visible = True 'wenn Sie sehen wollen, was vor sich geht ' wb.NewWindow() ws = wb.Sheets(1) For i = 1 To 3 For j = 1 To 3 Console.WriteLine("Zelle in Zeile {0} / Spalte {1} ={2}", _ i, j, ws.Cells(i, j).Value) Next Next ws.Cells(4, 1).Value = Now ' wb.Windows(wb.Windows.Count).Close() wb.Save() wb.Close() If xl.Workbooks.Count = 0 Then xl.Quit() End Sub
526
12 Spezialthemen
12.5.3 Beispiel – RichTextBox mit Word ausdrucken Die in Abschnitt 14.4.4 vorgestellte RichTextBox hat einen gravierenden Nachteil: man kann damit nichts ausdrucken. Das folgende Beispielprogramm umgeht dieses Problem: Es kopiert den gesamten Inhalt eines derartigen Steuerelements in ein neues Word-Dokument, stattet das Dokument mit einer Kopfzeile samt Seitenzähler aus und zeigt es in der Vorschau an (siehe Abbildung 12.5). Der Anwender kann das Dokument nun bequem ausdrucken und muss Word dann selbst beenden.
Abbildung 12.5: Text aus RichTextBox mit Word ausdrucken
Das Programm setzt voraus, dass ein Verweis auf die Bibliothek Microsoft.Word eingerichtet wurde. Mit CreateObject wird ein neues Objekt der Klasse Word.Document erzeugt. Der folgende Code, um den Inhalt der Zwischenablage einzufügen, das Dokument mit einer Kopfzeile auszustatten und in der Seitenvorschau anzuzeigen, wurde unter Word mit der Makroaufzeichnung entwickelt und dann in den VB.NET-Code eingebaut. Die meisten Änderungen betrafen die Konstanten, die in Word direkt verwendet werden können, in VB.NET aber nur bei genauer Angabe der Aufzählung, in der sie definiert sind. Damit nicht jeder Anweisung doc.Application vorangestellt werden muss, wurde With eingesetzt.
12.5 Externe Programme steuern (Automation)
527
' Beispiel spezial\automation-word Private Sub Button1_Click(...) Handles Button1.Click Dim doc As Word.Document ' Inhalt der RichTextBox in die Zwischenablage kopieren RichTextBox1.SelectAll() RichTextBox1.Copy() ' neues Word-Dokument erzeugen; dieses ist automatisch ' das aktive Dokument doc = CType(CreateObject("Word.Document"), Word.Document) With doc.Application .Selection.Paste() .ActiveWindow.ActivePane.View.Type = Word.WdViewType.wdPrintView .ActiveWindow.ActivePane.View.SeekView = _ Word.WdSeekView.wdSeekCurrentPageHeader .Selection.TypeText( _ Text:="Kopfzeile" & vbTab & vbTab & _"Seite ") .Selection.Fields.Add(Range:=.Selection.Range, _ Type:=Word.WdFieldType.wdFieldPage) .ActiveWindow.ActivePane.View.SeekView = _ Word.WdSeekView.wdSeekMainDocument ' Druckvorschau .Visible = True .ActiveDocument.PrintPreview() AppActivate(.ActiveWindow.Caption) End With End Sub
Wenn Sie möchten, dass der Text sofort und ohne Rückfrage ausgedruckt wird ohne dass Word dabei sichtbar wird, ersetzen Sie die drei letzten Zeilen durch den folgenden Code. Damit wird das Dokument gedruckt. Wenn der Ausdruck abgeschlossen ist, wird das Dokument durch Close geschlossen. Die CType-Konstruktion ist erforderlich, weil VB.NET aus nicht ganz nachvollziehbaren Gründen zwei Close-Methoden erkennt und nicht weiß, welche es einsetzen soll. Wenn es anschließend keine offenen Dokumente in Word gibt, wird das Programm durch Quit beendet (wobei derselbe Namenskonflikt wie bei Close auftritt). .ActiveDocument.PrintOut(False) 'im Vordergrund drucken CType(doc, Word._Document).Close( _ Word.WdSaveOptions.wdDoNotSaveChanges) If .Documents.Count = 0 Then CType(.Application, Word._Application).Quit() End If
528
12 Spezialthemen
12.6
Multithreading
12.6.1 Grundlagen
HINWEIS
Ein Thread ist ein Teilprozess eines Programms. Ein gewöhnliches Programm besteht aus nur einem einzigen Thread zur Ausführung des Programms. Multithreading bedeutet, die Programmausführung auf mehrere Threads zu verteilen; diese Threads werden dann quasi parallel ausgeführt. Neben den von Ihnen verwalteten Threads gibt es noch mindestens zwei weitere Threads, die von den .NET-Bibliotheken benötigt werden. Diese dienen unter anderem dazu, regelmäßig den Speicher von nicht mehr benötigten Objekten durch eine garbage collection freizugeben. Deswegen zeigt der Task-Manager selbst bei einer einfachen Konsolenanwendungen mindestens drei Threads an. Auf die durch .NET vorgegebenen Threads haben Sie im Regelfall keinen direkten Einfluss. Wenn in diesem Abschnitt von Threads die Rede ist, sind deshalb nur die vom Hauptprogramm verwalteten Threads gemeint.
Wie parallel die Ausführung wirklich ist, hängt auch von Ihrer Hardware ab. Auf einem gewöhnlichen PC mit einer CPU kann immer nur ein Thread ausgeführt werden. Bei einem Multithreading-Programm wechselt das Betriebssystem aber automatisch alle paar Millisekunden zwischen den Threads, so dass der Eindruck der Gleichzeitigkeit entsteht. Nur auf einem Rechner mit mehreren CPUs ist es theoretisch möglich, dass zwei Threads wirklich gleichzeitig ausgeführt werden.
Multithreading-Modelle Es gibt verschiedene Arten des Multithreadings. Der Großteil der .NET-Bibliothek sowie gewöhnliche VB.NET-Anwendungen (inklusive Konsolenanwendungen) verwenden so genanntes free threading (freies Threading); dort ist das Erstellen neuer Threads und der Wechsel zwischen den Threads problemlos möglich. Jeder Thread ist selbstständig. Es besteht aber auch die Möglichkeit, mehrere Threads in so genannte apartments zu gruppieren. Soweit sich mehrere Threads im selben Apartment befinden, vereinfacht sich dadurch der gegenseitige Zugriff auf Objekte (d.h., ein Thread kann Methoden eines Objekts nützen, das in einem anderen Thread erstellt wurde etc. ). Es gibt zwei Apartment-Modelle: •
Windows.Forms-Programme basieren auf dem Singlethread-Apartment-Modell (STA). Allerdings ergeben sich daraus Einschränkungen, wenn ein neuer (eigener) Thread, der sich außerhalb des apartments befindet, auf Fenster und Steuerelemente zugreifen will (siehe Abschnitt 15.2.7).
•
Für Server-Anwendungen empfiehlt sich die Verwaltung eines so genannten ThreadPools unter Zuhilfenahme der ThreadPool-Klasse. Dabei werden die Threads in einem
12.6 Multithreading
529
Multithread-Apartment (MTA) verwaltet. Diese Technik wird in diesem Buch allerdings
nicht beschrieben.
Wozu Multithreading? Je nach Anwendung kann Multithreading verschiedene Vorteile mit sich bringen: •
Bei Server-Anwendungen, in denen ein Programm gleichzeitig auf unterschiedliche Datenanfragen (üblicherweise aus dem Netzwerk) antworten soll, kann Multithreading die Effizienz steigern. Der Grund: Zur Beantwortung jeder Anfrage müssen üblicherweise Daten aus einer Datei oder aus einer Datenbank gelesen werden. Dabei treten meistens kleine Verzögerungen auf, die ein Single-Threaded-Programm untätig abwarten müsste. Bei einem Multithreading-Programm findet bei einer derartigen Wartezeit automatisch ein Thread-Wechsel statt, so dass ein anderer Teilprozess mit der Beantwortung einer anderen Anfrage beginnen kann. Beachten Sie, dass diese Darstellung ein wenig vereinfacht ist. (Die Idee sollte aber klar werden.) Tatsächlich ist die Entwicklung effizienter Server-Anwendungen eine ziemlich diffizile Sache: Wie werden die Anfragen auf unterschiedliche Threads verteilt? Welche Thread-Anzahl führt zu einer optimalen Effizienz? (Mit der Zahl der Threads steigt auch der Verwaltungs-Overhead.)
•
Bei interaktiven Programmen (z.B. bei einer gewöhnlichen Windows-Anwendung) kann durch Multithreading ein höherer Benutzerkomfort erreicht werden. Beispielsweise kann eine Berechnung in einem eigenen Thread gestartet werden, während der Haupt-Thread für die Benutzeroberfläche weiterläuft. Damit wird die Benutzeroberfläche nicht durch die Berechnung blockiert. (Denselben Effekt haben Sie beispielsweise, wenn Sie in Word ein Dokument im Hintergrund ausdrucken und währenddessen ein anderes Dokument bearbeiten oder wenn Sie in Outlook E-Mails versenden und zugleich eine neue verfassen etc.)
ACHTUNG
Beachten Sie, dass Multithreading ein Programm nicht generell schneller macht – im Gegenteil! Multithreading bringt einigen Overhead mit sich, sowohl auf Betriebssystemebene durch die Thread-Wechsel als auch in Ihrem Programm, in dem die Threads miteinander kommunizieren und sich oft auch synchronisieren müssen. Wenn Sie also eine Aufgabe möglichst rasch erledigen möchten, ist der herkömmliche Single-Threaded-Lösungsansatz der effizienteste. Die Programmierung stabiler Multithreading-Anwendungen ist relativ kompliziert, insbesondere dann, wenn mehrere Threads gemeinsame Daten ändern müssen (was sich nur selten vermeiden lässt). Erschwerend kommt hinzu, dass die Fehlersuche in Multithreading-Anwendungen mühsam ist, dass eventuelle Fehler möglicherweise nur sehr selten auftreten und daher schwerer zu finden sind. Programmieren Sie also nicht einfach los, sondern lesen Sie sich vorher ausführlich in die Materie ein! (Dieser Abschnitt stellt nur eine erste Einführung dar!)
530
12 Spezialthemen
Wie eingangs bereits erwähnt, kann dieses Buch nur eine Einleitung in das Thema Multithreading geben. Weitere Basisinformationen finden Sie in der Hilfe, wenn Sie nach Multithreading in Visual Basic suchen:
VERWEIS
ms-help://MS.VSCC/MS.MSDNVS.1031/vbcn7/html/vaconThreadingInVisualBasic.htm
Wenn Sie sich speziell für die Komponentenprogrammierung interessieren, suchen Sie am besten nach Multithreading in Komponenten: ms-help://MS.VSCC/MS.MSDNVS.1031/vbcon/html/vbconScalabilityMultithreadingInComponents.htm
Wenn Sie sich für Hintergründe und fortgeschrittene Programmiertechniken interessieren (z.B. für die Verwaltung von Thread-Pools), suchen Sie nach Threads und Threading. Sie werden auf eine ausführliche Beschreibung des .NET-Threading-Modells stoßen: ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpconthreading.htm
12.6.2 Programmiertechniken Neue Threads starten Fast alle Klassen zur Verwaltung von Multithreading-Anwendungen befinden sich im Namensraum System.Threading der Standardbibliothek mscorlib.dll. Um eine Prozedur in einem neuen Thread auszuführen, müssen Sie zuerst ein neues Thread-Objekt erzeugen. Dabei geben Sie als Parameter die Adresse der auszuführenden Prozedur oder Methode an. Die Prozedur darf keine Parameter haben. Funktionen bzw. Methoden mit einem Rückgabewert sind ebenfalls nicht zulässig. Anschließend führen Sie für das Objekt die Methode Start aus. Dim mythread As New Threading.Thread(AddressOf myprocedure) mythread.Start() Start übergibt den neuen Thread an das Betriebssystem. Dieses bestimmt, wann der Thread
tatsächlich gestartet wird. (Das ist meist nicht sofort der Fall, sondern erst nach ein paar Millisekunden.) Der neue Thread läuft nun parallel zum Hauptprogramm, d.h., die Programmausführung wechselt alle paar Millisekunden zwischen dem Hauptprogramm und der Prozedur myprocedure. Damit verlangsamt sich natürlich sowohl das Hauptprogramm als auch die Prozedur, weil die beiden Programmteile sich ja nun die verfügbare CPU-Zeit teilen müssen. Wenn eines der beiden Programmteile vorübergehend blockiert ist (etwa weil es auf das Öffnen einer Datei, die Herstellung einer Datenbankverbindung oder eine Benutzereingabe wartet), wird der andere Programmteil während dieser Zeit fast ungehindert ausgeführt.
12.6 Multithreading
531
Einführungsbeispiel Abbildung 12.6 zeigt die Ausgaben eines ersten, sehr einfachen Multithreading-Beispiels: Sowohl das Hauptprogramm als auch die Prozedur sub1, die in einem eigenen Thread ausgeführt wird, durchlaufen eine lange Schleife und geben während dieser Zeit 200 Mal den Buchstaben M (main) bzw. 1 (sub1) im Konsolenfenster aus. main wartet am Ende der Schleife durch Join darauf, bis auch die Schleife des zweiten Threads zu Ende ist.
Abbildung 12.6: Ein erstes Multithreading-Programm
' Beispiel spezial\multithread-intro Const max_loop As Integer = 1000 * 1000 * 100 Const interval As Integer = max_loop \ 200 Sub Main() Dim i As Integer Dim mythread As New Threading.Thread(AddressOf sub1) mythread.Start() For i = 0 To max_loop If i Mod interval = 0 Then Console.Write("M") Next mythread.Join() 'auf das Ende von mythread warten Console.WriteLine(vbCrLf + "Return drücken") Console.ReadLine() End Sub Sub sub1() Dim i As Integer For i = 0 To max_loop If i Mod interval = 0 Then Console.Write("1") Next End Sub
Periodischer Prozeduraufruf Anstatt eine Prozedur einmal in einem eigenen Thread zu starten, besteht auch die Möglichkeit, dies periodisch zu tun. Das ist vor allem dann sinnvoll, wenn die Prozedur eine Kontroll- oder Protokollierungsaufgabe übernehmen soll, die sehr rasch erledigt werden kann. Zu diesem Zweck müssen Sie zuerst ein Objekt der Delegate-Klasse TimerCallback erzeugen und dabei die Adresse der aufzurufenden Prozedur oder Methode angeben.
532
12 Spezialthemen
Dim mytimerDelegate As New Threading.TimerCallback( _ AddressOf method1)
Um die periodischen Aufrufe zu starten, erzeugen Sie ein Objekt der Timer-Klasse. Mit state wird ein beliebiges Objekt (oder Nothing) angegeben, das bei jedem Aufruf an die Prozedur oder Methode übergeben wird. n1 gibt an, nach wie vielen Millisekunden die Prozedur zum ersten Mal ausgeführt werden soll. (0 bedeutet, so schnell wie möglich.) n2 gibt das Intervall an, alle wie viel Millisekunden die Prozedur aufgerufen werden soll. Die Einstellungen n1 und n2 können später durch die Change-Methode des Timer-Objekts verändert werden. Dim tm As New Threading.Timer(mytimerDelegate, state, n1, n2)
Die aufzurufende Methode muss folgendermaßen deklariert werden. (Achten Sie auf die Parameterliste! Anders als bei einfachen Thread-Aufrufen ist hier die Übergabe eines Objekts zwingend vorgesehen.)
HINWEIS
Der periodische Prozeduraufruf erfolgt in einem Thread mit der Eigenschaft IsBackground = True. Deswegen enden dieser Thread und somit auch die periodischen Aufrufe automatisch mit dem Ende des Programms.
VERWEIS
Sub method1(ByVal state As Object)
Ein Beispiel für die Anwendung der Threading.Timer-Klasse finden Sie in Abschnitt 9.3.5. Beachten Sie, dass es speziell für Windows-Anwendungen eine eigene TimerKlasse gibt, die in Abschnitt 14.10.2 beschrieben wird.
Kommunikation zwischen Hauptprogramm und Thread An die mit mythread.Start() gestartete Prozedur oder Methode können keine Parameter übergeben werden. Ebensowenig kann die Prozedur nicht (wie eine Funktion) eine Ergebnis an das Hauptprogramm zurückliefern. Daher müssen andere Wege zur Kommunikation gesucht werden. Der einfachste Weg, an einen Thread Daten zu übergeben und später im Hauptprogramm Ergebnisse zu empfangen, bietet die Definition einer eigenen Klasse: Um eine Operation in einem neuen Thread zu starten, erzeugen Sie zuerst ein Objekt dieser Klasse und übergeben die Startdaten mittels Klassenvariablen oder Eigenschaften und Methoden. Dann verwenden Sie den neuen Thread, um eine Methode der Klasse auszuführen. Am Ende der Methode erfolgt die Rückmeldung an das Hauptprogramm über eine Ereignisprozedur. Dieser Mechanismus wird durch das folgende Beispiel veranschaulicht.
12.6 Multithreading
533
' Beispiel spezial\multithread-event Module Module1 Dim WithEvents calc As class1 Sub Main() Dim i As Integer Dim mythread As Threading.Thread calc = New class1() calc.data = 123 mythread = New Threading.Thread(AddressOf calc.method1) mythread.Start() ' ... Hauptprogramm fortsetzen End Sub Public Sub calc_Done(ByVal obj As Object, ByVal result As Integer) _ Handles calc.Done Console.WriteLine("calc_Done: result={0}", result) ' ... Ergebnis verarbeiten End Sub End Module Class class1 Public data As Integer Public result As Integer Public Event Done(ByVal obj As Object, ByVal result As Integer) Public Sub method1() ' ... eine komplizierte Berechnung result = data - 1 RaiseEvent Done(Me, result) End Sub End Class
Thread-Ausführung vorübergehend unterbrechen (Sleep) Wenn Sie den aktuellen Thread für einige Zeit unterbrechen möchten, können Sie dazu die Sleep-Methode verwenden. Als Parameter geben Sie die gewünschte Zeit in Millisekunden an. ' 5 Sekunden warten Threading.Thread.Sleep(5000) Sleep kann auch in gewöhnlichen (Single-Threaded-)Programmen verwendet werden und bewirkt dann, dass das gesamte Programm während der angegebenen Zeit ruht. Sleep ist
auf jeden Fall eventuellen Warteschleifen vorzuziehen, weil es keine CPU-Zeit verbraucht und stattdessen die Rechenzeit anderen Threads bzw. anderen Programmen des Computers zur Verfügung stellt.
HINWEIS
534
12 Spezialthemen
Sleep gilt immer für den gerade aktiven Thread! Wenn Sie im Einführungsbeispiel in main die Anweisung mythread.Sleep(1000) ausführen, dann gilt Sleep dennoch für main (nicht für mythread!).
Auf das Ende eines anderen Threads warten Wenn Sie im aktuellen Code darauf warten möchten oder müssen, bis ein anderer Thread mythread zu Ende ist, führen Sie mythread.Join() aus.
Threads vorübergehend anhalten (Suspend und Resume) Mit mythread.Suspend() können Sie die Ausführung eines Threads vorübergehend stoppen. mythread.Resume() setzt die Ausführung zu einem späteren Zeitpunkt fort. Ein Thread kann sich selbst in den Haltezustand versetzen (Threading.Thread.CurrentThread.Suspend()). Das ist aber nur dann ratsam, wenn sichergestellt ist, dass ein anderer Thread den ruhenden Thread wieder aufweckt.
Threads beenden oder abbrechen (Abort) Ein Thread endet automatisch, wenn das Ende der gestarteten Prozedur oder Methode erreicht wird. Um den aktuell laufenden Thread zu beenden, brauchen Sie also nur Exit Sub auszuführen. Von außen kann ein Thread durch mythread.Abort() beendet werden. Abort bewirkt, dass im betroffenen Thread eine ThreadAbortException ausgelöst wird. Außerdem wird der ThreadStatus des Threads auf AbortRequested gesetzt. An die Abort-Methode kann optional ein beliebiges Objekt übergeben werden, das bei der Auswertung des ThreadAbortException-Objekts aus der Eigenschaft ExceptionState entnommen kann. Das Objekt kann beispielsweise dazu dienen, den Thread über die Gründe des Abbruchs zu informieren. Die Reaktion eines Threads auf eine ThreadAbortException ist aus mehreren Gründen ungewöhnlich: •
Wenn der Code des Threads nicht abgesichert ist (wenn es also keine Try-Catch-Konstruktion gibt), wird der Thread ohne Fehlermeldung still beendet. Das Gesamtprogramm wird fortgesetzt (sofern es noch andere Vordergrund-Threads gibt). Das ist insofern untypisch, als nicht abgesicherte Exceptions im Allgemeinen zu einer Fehlermeldung und dann zum Programmende führen. Das außergewöhnliche Verhalten auf eine ThreadAbortException kann auch im Dialog DEBUGGEN|AUSNAHMEN nachvollzogen werden: Dort lautet die Einstellung (im Unterschied zu anderen Exceptions), dass das Programm ohne Fehlermeldung fortgesetzt werden soll.
•
Die ThreadAbortException kann wie jede andere Exception durch Try-Catch abgefangen werden. Am Ende der Try-Konstruktion wird der Fehler aber neuerlich ausgelöst und
12.6 Multithreading
535
der Thread somit trotz der Try-Konstruktion beendet. Sie können somit ein geordnetes Ende der Prozedur erreichen, offene Ressourcen schließen etc., aber im Gegensatz zu gewöhnlichen Exceptions gilt der Fehler durch die Try-Konstruktion nicht als behoben. Wenn Sie einen Thread trotz dieses merkwürdigen Verhaltens nach einer ThreadAbortException fortsetzen möchten, müssen Sie innerhalb des Catch-Blocks die Methode Threading.Thread.CurrentThread.ResetAbort() ausführen. ResetAbort darf nur dann verwendet werden, wenn für Ihr Programm erweiterte Sicherheitseinstellungen gelten (was oft nicht der Fall ist). Generell ist es nur in sehr seltenen Fällen sinnvoll, einen Thread trotz Abort-Aufforderung fortzusetzen. •
Der Finally-Block einer Try-Konstruktion wird auf jeden Fall ausgeführt, selbst dann, wenn die ThreadAbortException nicht durch Catch abgefangen wird (d.h., wenn es in der Try-Konstruktion gar keinen Catch-Block gibt oder wenn es nur Catch-Blöcke für andere Fehler gibt). Beachten Sie, dass ResetAbort im Finally-Block nicht mehr wirksam ist.
Wenn der Code eines Threads Dispose-Objekte erzeugt, Datenbankverbindungen herstellt, Dateien öffnet etc., sollte (zumindest) eine Fehlerabsicherung in der folgenden Form vorliegen: Sub method1() Try ... der eigentliche Code Finally ... Aufräumarbeiten, die in jedem Fall ausgeführt werden (auch dann, wenn eine ThreadAbortException auftritt) End Try End Sub
HINWEIS
mythread.Abort bewirkt also nicht, dass der betreffende Thread sofort beendet wird! Zum einen wird die ThreadAbortException erst dann tatsächlich ausgelöst, wenn die
Ausführung des betreffenden Threads aufgrund eines Thread-Wechsels fortgesetzt wird – und bis dahin können einige Millisekunden verstreichen. Zum anderen liegt es ja nun in der Verantwortung des Threads, wie es auf die ThreadAbortException reagiert. Es kann beispielsweise sein, dass die erforderlichen Aufräumarbeiten (z.B. zum Schließen einer Datei) einige Zeit in Anspruch nehmen. Aus diesem Grund ist es fast immer empfehlenswert, das tatsächliche Thread-Ende mit Join abzuwarten, also: mythread.Abort() mythread.Join()
Programmende Ein Multithreading-Programm läuft normalerweise so lange, bis alle Threads beendet sind. Wenn Sie also in einem Konsolenprogramm in main einige neue Threads starten und main dann verlassen, läuft das Programm so lange weiter, bis der letzte der Threads beendet ist.
536
12 Spezialthemen
VORSICHT
Wenn Sie ein geordnetes Programmende in main erreichen möchten, können Sie dort mit Join auf das Ende der anderen Threads warten. Radikaler ist es, die anderen Threads durch Abort gewaltsam zu beenden. Schließlich besteht die Möglichkeit, die neuen Threads gleich beim Start als Hintergrund-Threads zu kennzeichnen (mythread.IsBackGround = True). Das bewirkt, dass mit dem Ende des letzten Vordergrund-Threads alle Hintergrund-Threads automatisch durch Abort beendet werden. (Per Default gelten der Start-Thread sowie alle neuen Threads als Vordergrund-Threads.) Solange es unterbrochene Vordergrund-Threads gibt (Suspend), kann das Programm nicht beendet werden. Es darf daher auf keinen Fall passieren, dass als einziger Thread ein unterbrochener Thread übrig bleibt. (Dasselbe gilt natürlich auch für mehrere unterbrochene Threads.) Das Programm befindet sich dann in einer Sackgasse, aus der es nicht mehr herausfindet: Die unterbrochenen Threads warten darauf, dass irgendwo Resume ausgeführt wird, aber es gibt im Programm gar keine Stelle mehr, an der noch Code ausgeführt wird.
Thread-Eigenschaften Jedes Thread-Objekt ist mit einer Reihe von Eigenschaften ausgestattet, die Auskunft über den Zustand des Threads geben und zum Teil auch eine Veränderung ermöglichen. Priority gibt an, welche Priorität der Thread relativ zu anderen hat. Per Default ist jeder Thread mit ThreadPriority.Normal eingestuft. Durch die Einstellungen Below- oder AboveNormal kann ein Thread entsprechend seiner Wichtigkeit benachteiligt oder bevorzugt werden. Wenn die CPU voll ausgelastet wird, wird einem derartigen Thread daher weniger oder mehr Rechenzeit zugeordnet. CurrentCulture verweist auf ein CultureInfo-Objekt, dessen Inhalt unter anderem die Metho-
den zur Formatierung von Zahlen, Daten und Zeiten beeinflusst. Normalerweise übernehmen alle neu gestarteten Threads die Einstellungen des Start-Threads, aber es ist prinzipiell auch möglich, unterschiedliche Threads mit unterschiedlichen internationalen Einstellungen auszuführen.
VERWEIS
ThreadState gibt den aktuellen Zustand des Threads an. Mögliche Werte sind die Konstanten der ThreadState-Aufzählung, z.B. Running, Aborted etc. IsAlive liefert die Quintessenz von ThreadState, nämlich False, wenn ThreadState=Unstarted, Stopped oder Aborted, sonst True.
In der Online-Hilfe finden Sie ausführliche Informationen darüber, unter welchen Umständen ein Thread welchen Zustand einnimmt, wenn Sie nach Statuswerte der Threadaktivität suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpconthreadactivitystates.htm
TIPP
12.6 Multithreading
537
Am einfachsten ist der Zugriff auf diese Eigenschaften natürlich über eine Variable mit einem Thread-Objekt (mythread.ThreadState etc.). Wenn Sie auf den gerade aktiven Thread zugreifen möchten (den, der die aktuelle Codeanweisung ausführt), können Sie dazu Threading.Thread.CurrentThread verwenden.
Liste aller Threads ermitteln Wenn Sie wissen möchten, welche Threads es im aktuellen Programm gibt, können Sie die Threads-Eigenschaft der Klassse Process des Namensraums System.Diagnostics auswerten (Bibliothek System.dll). Vorher müssen Sie mit GetCurrentProcess das Process-Objekt des aktuellen Programms ermitteln. Die Threads-Aufzählung liefert ProcessThread-Objekte, die weit mehr Eigenschaften aufweisen als die Thread-Objekte des Threading-Namensraum. Die folgenden Zeilen zeigen beispielhaft die Auswertung. Dim pr As Diagnostics.Process = _ Diagnostics.Process.GetCurrentProcess() Dim tr As Diagnostics.ProcessThread For Each tr In pr.Threads Console.WriteLine("id={0} starttime={1}", tr.Id, tr.StartTime) Next
Debugging
TIPP
Wenn die Programmausführung durch Stop, durch einen Haltepunkt oder durch einen Fehler in einem der Threads unterbrochen und der Debugger angezeigt wird, dann werden damit alle Threads unterbrochen. Im Threads-Fenster (DEBUGGEN|FENSTER|THREADS) ist der zuletzt aktive Thread markiert. Sie können dort in einen anderen Thread wechseln. Es erleichtert die Fehlersuche erheblich, wenn Sie jedem Thread beim Erzeugen einen Namen geben (myThread.Name="..."). Der Name wird im Threads-Fenster angezeigt.
Beachten Sie, dass die Entwicklungsumgebung bzw. der Debugger das Laufzeitverhalten von Multithreading-Programmen beeinflusst! Wenn Sie das Programm außerhalb der Entwicklungsumgebung starten, erfolgt der Thread-Wechsel bisweilen deutlich rascher als innerhalb der Entwicklungsumgebung. Wenn Sie Multithreading-Anwendungen entwickeln, sollten Sie das Programm also unbedingt auch außerhalb der Entwicklungsumgebung ausführlich testen!
538
12 Spezialthemen
VERWEIS
Beispiele Dieser Abschnitt hat lediglich Multithreading-Grundlagen vermittelt. Einige konkrete Anwendungen finden Sie im Stichwortverzeichnis unter Multithreading. Werfen Sie insbesondere einen Blick in die Abschnitte 9.3.5 und 15.2.7! Dort geht es um die korrekte Verwendung von Aufzählungsobjekten und um die Gestaltung von Multithreading-Windows-Programmen. Beachten Sie, dass auch jede asynchrone Operation (z.B. das asynchrone Lesen oder Schreiben von Dateien, siehe Abschnitt 10.7) intern auf Multithreading basiert.
Syntaxzusammenfassung System.Threading.Thread-Klasse – Methoden Abort( [obj] )
fordert den Thread dazu auf zu enden. Im Thread kommt es dadurch zu einer ThreadAbortException, wobei die optionalen Daten obj übergeben werden.
Join()
wartet auf das Ende des Threads.
ResetAbort()
setzt die Ausführung des Threads trotz einer ThreadAbortException fort.
Resume()
setzt einen unterbrochenen Thread fort.
Sleep()
unterbricht die Ausführung des gerade aktiven Threads für eine bestimmte Zeit. (Während dieser Zeit können andere Threads ausgeführt werden.)
Start()
startet den Thread.
Suspend()
hält die Ausführung eines Threads vorübergehend an. Der Thread kann mit Resume fortgesetzt werden.
System.Threading.Thread-Klasse – Eigenschaften ApartmentState
gibt an, ob der Thread Teil eines in einem Single- oder Multithread-Apartments (STA oder MTA) ist.
CurrentCulture
gibt das CultureInfo-Objekt des Threads an, das unter anderem Einfluss auf Konvertierungs- und Formatierungsmethoden hat (z.B. die Darstellung von Datum und Uhrzeit).
CurrentThread
verweist auf das Thread-Objekt des gerade laufenden Threads.
IsAlive
gibt an, ob der Thread grundsätzlich aktiv ist (ThreadState ungleich Unstarted, Stopped oder Aborted).
12.6 Multithreading
539
System.Threading.Thread-Klasse – Eigenschaften IsBackground
gibt an, ob der Thread ein Hintergrund-Thread ist. In diesem Fall endet er automatisch mit dem Hauptthread.
Name
gibt den Namen des Threads an (per Default leer).
Priority
gibt die Priorität des Threads an (per Default ThreadPriority.Normal).
ThreadState
gibt den aktuellen Zustand des Threads durch eine Konstante der ThreadState-Aufzählung an (z.B. Running, Stopped, Aborted, WaitSleepJoin).
12.6.3 Synchronisierung von Threads Wenn mehrere Threads auf gemeinsame Daten zugreifen müssen und zumindest einer der Threads die Daten auch verändern muss, sind Probleme im wörtlichen Sinne vorprogrammiert. Es kann vorkommen, dass ein Thread unterbrochen wird, während er die Daten ändert und nun ein anderer Thread die unvollständig veränderten Daten zu lesen versucht: Das Ergebnis sind korrupte Daten (die meist unmittelbar zu Fehlern führen) oder falsche Daten (was viel schlimmer ist, weil dieser Fall oft lange unbemerkt bleibt). Beachten Sie, dass der Thread-Wechsel vom Betriebssystem durchgeführt wird und zu jedem Zeitpunkt erfolgen kann, selbst während einer ganz elementaren Operation (z.B. x += 1)! Um derartige Probleme zu vermeiden, müssen die Threads synchronisiert werden. Das bedeutet, dass ein Thread mit dem Zugriff auf gemeinsame Daten warten muss, bis die anderen Threads fertig sind. Den einfachsten Weg bietet hierfür das VB-Konstrukt SyncLock: Damit wird ein Objekt angegeben, auf das ein Programmteil den alleinigen Zugriff beansprucht. Wenn das Objekt bei der Ausführung von SyncLock bereits durch einen anderen Thread blockiert ist, muss der aktuelle Thread warten, bis das Objekt wieder freigegeben wird. Sobald das Objekt frei ist, bekommt der aktuelle Thread den alleinigen Zugriff auf das Objekt. Nun müssen also alle anderen Threads warten. SyncLock data ... Code, der das Objekt data verändert End SyncLock
Mit SyncLock muss ein Objekt eines Referenztyps angegeben werden. ValueType-Variablen für elementare Datentypen wie Integer oder für Strukturen sind also ungeeignet. StringVariablen sind nur dann geeignet, wenn die Zeichenkette nicht geändert wird. (Bei jeder Veränderung wird ein neues String-Objekt erzeugt, womit der Schutz hinfällig wird.) Mit SyncLock kann nur ein Objekt angegeben werden. Wenn innerhalb der SyncLock-Codes mehrere Objekte bearbeitet bzw. verändert werden, dann müssen sich alle Codeteile auf ein gemeinsames Objekt für SyncLock einigen. (SyncLock ist ja kein Schutz gegen Veränderungen, sondern ein Schutz gegen die gleichzeitige Ausführung von Code. Insofern ist es
540
12 Spezialthemen
ganz egal, welches Objekt mit SyncLock angegeben wird, solange es nur immer dasselbe ist.)
HINWEIS
VORSICHT
.NET-intern verwendet SyncLock die Monitor-Klasse. Das Objekt wird durch Monitor.Enter(obj) gesperrt und durch Monitor.Exit(obj) wieder freigegeben. Die Online-Hilfe rät explizit davon ab, SyncLock auf Fenster oder Steuerelemente von Windows.Forms-Anwendungen anzuwenden. Sie riskieren dadurch einen deadlock (also einen Zustand, bei dem sich zwei Threads gegenseitig so blockieren, dass keiner mehr fortgesetzt werden kann). Lesen Sie zur Multithreading-Programmierung von Windows.Forms-Programmen unbedingt Abschnitt 15.2.7.
Vor allem wenn mehrere Threads im Spiel sind, wird eine derartige Synchronisierung zunehmend ineffizient. Sie sollten daher darauf achten, den Objektzugriff nicht länger als unbedingt erforderlich durch SyncLock zu blockieren. In manchen Fällen reicht die Sicherheit, dass einfache Operationen (z.B. das Vergrößern oder Verkleinern eines Zählers) nicht durch einen Thread-Wechsel unterbrochen werden. Zu diesem Zweck stellt die Interlock-Klasse die Methoden Increment, Decrement, Exchange und CompareExchange für einige elementare Datentypen zur Verfügung. Die Verwendung dieser Methoden ist deutlich effizienter als SyncLock.
Synchronisierungsbeispiel Das folgende Beispiel illustriert die Problematik. Als gemeinsame Daten dienen die Elemente des Integer-Felds data. Im main wird die Prozedur changearray in einem eigenen Thread gestartet. In dieser Prozedur werden unzählige Male zwei zufällige Elemente des Felds ausgewählt; eines dieser Elemente wird um einen zufälligen Wert erhöht, ein zweites um denselben Wert verkleinert. Die Summe aller Feldelemente beträgt daher immer 0.
Abbildung 12.7: Fehler aufgrund mangelnder Thread-Synchronisation
12.6 Multithreading
541
Solange der changearray-Thread läuft (mythread.IsAlive), wird in main immer wieder die Prozedur calc_sum aufgerufen. Diese Prozedur berechnet die Summe der Feldelemente. Wenn das Ergebnis ungleich 0 ist, wird der Wert ausgegeben. Abbildung 12.7 beweist, dass dieser Fall bei einem Testdurchlauf 21 Mal aufgetreten ist – eine Fehlerwahrscheinlichkeit von 0,0063 Prozent. (Das Ergebnis sieht natürlich jedes Mal ein wenig anders aus – mal mit mehr, mal mit weniger Fehlern. Es sollte aber aber klar sein, wie klein die Chance ist, einen derartigen Fehler durch Debugging klar nachzuvollziehen.) ' Beispiel spezial\multithread-syncerror Const max_loop As Integer = 1000 * 1000 * 10 Const arr_size As Integer = 1000 Dim data(arr_size - 1) As Long Dim error_counter As Long Sub Main() Dim counter As Integer Dim starttime As Date = Now Dim mythread As New Threading.Thread(AddressOf changearray) mythread.Start() While mythread.IsAlive counter += 1 calc_sum() End While Console.WriteLine("Summe wurde {0} Mal berechnet", counter) Console.WriteLine("Dabei traten {0} Fehler auf", error_counter) Console.WriteLine("Zeit: {0}", Now.Subtract(starttime)) End Sub ' vergrößert ein Element des Feldes um tmp, ' reduziert ein anderes Element ebenfalls um tmp Sub changearray() Dim i As Integer Dim index1, index2 As Integer Dim tmp As Integer Dim rand As New Random() For i = 0 To max_loop tmp = rand.Next(100) index1 = rand.Next(arr_size) index2 = rand.Next(arr_size) ' SyncLock data data(index1) += tmp '(1) data(index2) -= tmp '(2) ' End SyncLock Next End Sub
542
12 Spezialthemen
' die Summe des Felds sollte immer 0 sein Sub calc_sum() Dim i As Integer Dim sum As Long ' SyncLock data For i = 0 To arr_size - 1 sum += data(i) Next 'End SyncLock If sum 0 Then Console.WriteLine("Summe: {0}", sum) error_counter += 1 End If End Sub
Die Fehlerursache sollte leicht verständlich sein: Wenn der changearray-Prozess genau zwischen den Anweisungen (1) und (2) unterbrochen wird und nun main die Feldsumme berechnet, ergibt sich eine Summe ungleich 0. Die Lösung des Problems ist im Code bereits angedeutet: Die beiden Anweisungen zur Änderung der Feldelemente sowie die Schleife zur Berechnung der Summe müssen durch SyncLock gekapselt werden. Damit gehören die Rechenfehler der Vergangenheit an. Der dramatische Anstieg der Rechenzeit auf ein Mehrfaches hat übrigens weniger mit dem Locking-Overhead zu tun als damit, dass die relativ zeitaufwendige Summenberechnung wegen der SyncLock-Anweisung gegenüber dem kurzen SyncLock-Block in changearray bevorzugt wird und unverhältnismäßig viel Rechenzeit zugeordnet bekommt. Aus diesem Grund wird die Summe sehr viel öfter als bisher berechnet.
Konflikte durch Mehrfachausführung von Code Was passiert, wenn ein und diesselbe Prozedur von mehreren Threads gleichzeitig ausgeführt wird? Die Dokumentation zu dieser Frage ist eher spärlich, weswegen ich ein bisschen experimentiert habe. Dabei hat sich gezeigt, dass lokale Variablen, die innerhalb einer Prozedur deklariert werden, tatsächlich lokal sind – also auch lokal für jeden Thread. Im folgenden Beispielprogramm wird die Prozedur sub1 durch zwei Threads beinahe gleichzeitig gestartet. Die Daten für die Schleifenvariablen i und j und die beiden Zähler k und x werden für jeden Thread an unterschiedlichen Orten im Speicher abgelegt. Daher kommen sich die beiden Threads nicht in die Quere und laufen vollkommen unabhängig voneinander ab. ' Beispiel spezial\multithread-conflicts Const loopsize As Integer = 1000 * 1000 * 10 Sub Main() Dim counter As Integer Dim starttime As Date = Now Dim mythread1 As New Threading.Thread(AddressOf sub1)
12.6 Multithreading
543
Dim mythread2 As New Threading.Thread(AddressOf sub1) mythread1.Name = "1" mythread2.Name = "2" mythread1.Start() mythread2.Start() mythread1.Join() mythread2.Join() ... Fortsetzung siehe etwas weiter unten End Sub Sub sub1() Dim i, j, k As Integer Dim x As Double For i = 1 To 10 For j = 1 To loopsize k += 1 x += Math.Sin(k) Next Console.WriteLine("thread={0}: i={1}, k={2}", _ Threading.Thread.CurrentThread.Name, i, k) Next End Sub
Thread-sichere Klassen und Methoden Bei der Beschreibung der .NET-Klassen wird immer wieder darauf hingewiesen, welche Methoden der Klasse Thread-sicher sind und welche nicht. Was bedeutet das aber? Thread-sicher bedeutet, dass Methode desselben Objekts von mehreren Threads quasi gleichzeitig ausgeführt werden dürfen, ohne dass es dabei zu Konflikten kommt. Eine kleine Variation des obigen Beispiels beweist, dass die Thread-Sicherheit durchaus nicht selbstverständlich ist. In den Threads mythread3 und -4 wird jeweils die Methode sub1 desselben Objekts der Klasse class1 ausgeführt. i, j, k und x sind nun Klassenvariablen, die von allen Threads gemeinsam genutzt werden. Aus diesem Grund kommt es nun zu Zugriffskonflikten; das Programm liefert nun eine ziemlich wirre Bildschirmausgabe (siehe Abbildung 12.8). Natürlich werden Schleifenvariablen üblicherweise auch bei Methoden von Klassen lokal definiert und bereiten dann keine Probleme. Aber bei den meisten Klassen gibt es Klassenvariablen, die von Methoden und Eigenschaften gemeinsam verwendet werden; welche Probleme dadurch entstehen können, wird durch das folgende Beispielprogramm demonstriert. (Dieselben Probleme würden natürlich auch dann auftreten, wenn Sie im vorigen Beispiel die die Zeilen mit der Variablendeklaration außerhalb von sub1 durchgeführt hätten. Die Probleme haben also nichts mit der Frage Modul versus Klasse zu tun, sondern damit, ob es gemeinsam genutzte Variablen gibt.)
544
12 Spezialthemen
' Beispiel spezial\multithread-conflicts Sub Main() ... Fortsetzung von oben Dim myobj As New class1() Dim mythread3 As New Threading.Thread(AddressOf myobj.sub1) Dim mythread4 As New Threading.Thread(AddressOf myobj.sub1) mythread3.Name = "3" mythread4.Name = "4" mythread3.Start() mythread4.Start() mythread3.Join() mythread4.Join() End Sub Class class1 Dim i, j, k As Integer Dim x As Double Sub sub1() For i = 1 To 10 For j = 1 To loopsize k += 1 x += Math.Sin(k) Next Console.WriteLine("thread={0}: i={1}, k={2}", _ Threading.Thread.CurrentThread.Name, i, k) Next End Sub End Class
Abbildung 12.8: Die Klassenvariablen i, j und k werden von meheren Threads gleichzeitig benutzt
12.6 Multithreading
545
Schlussfolgerungen für die Programmierung eigener Klassen Wenn Sie selbst Klassen programmieren möchten, die Thread-sicher sind, müssen Sie jede Methode der Klasse, die auf gemeinsame Klassenvariablen zugreift, durch SyncLock Me absichern. (Shared-Methoden, die keine gemeinsamen Daten verwenden, müssen dagegen nicht abgesichert werden.) Class class1 Dim ... Sub sub1() SyncLock Me ... Code mit Zugriff auf gemeinsame Klassenvariablen End SyncLock End Sub End Class
VERWEIS
Diese Vorgehensweise ist allerdings insofern unbefriedigend, weil die vielen SyncLock-Anweisungen die Anwendung der Klasse in Multithreading-Anwendungen sehr ineffizient machen und unter Umständen Deadlock-Probleme verursachen können. (Ein Deadlock ist so ziemlich das Schlimmste, was in einer Multithreading-Anwendung passieren kann: Thread A wartet, dass Thread B Daten freigibt, und umgekehrt. Das ist eine Pattsituation, aus der es keinen Ausweg gibt.) Aus diesem Grund sind die meisten .NET-Klassen nicht Thread-sicher und es bleibt dem Anwender der Klasse überlassen, die Methoden so aufzurufen, dass es keine Konflikte geben kann. Weitere Informationen zum Thema Thread-Sicherheit finden Sie, wenn Sie nach Entwurfsrichtlinien für das Threading oder nach Threadsichere Komponenten suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/cpgenref/html/cpconthreadingdesignguidelines.htm ms-help://MS.VSCC/MS.MSDNVS.1031/vbcon/html/vbconThread-SafeComponents.htm
Schlussfolgerungen für Anwender von nicht Thread-sicheren Methoden Wenn Sie in einer Multithreading-Anwendung nicht Thread-sichere Methoden anwenden (und das ist der Regelfall!), müssen Sie darauf achten, dass es ausgeschlossen ist, dass unterschiedliche Threads gleichzeitig Methoden oder Eigenschaften eines gemeinsamen Objekts ausführen. Die einfachste Lösung besteht darin, für jeden Thread eigene Objekte zu erzeugen. Wenn das nicht möglich ist und die Threads auf gemeinsame Objekte zugreifen müssen, dann müssen Sie vor dem Aufruf derartiger Methoden die bereits beschriebenen Synchronisationsmechanismen anwenden (also SyncLock).
546
12 Spezialthemen
12.7
API-Funktionen verwenden (Declare)
12.7.1 Grundlagen API steht für Application Programming Interface und bezeichnet die Programmierschnittstelle des Betriebssystems. Diese Schnittstelle besteht aus einer riesigen Zahl von Funktionen, die sich in diversen DLL-Dateien befinden. (Beispielsweise enthält gdi32.dll grundlegende Grafikfunktionen.) DLL ist die Kurzform für Dynamic Link Libraries und bezeichnet Bibliotheken, die bei Bedarf vom Betriebssystem geladen werden. In den Zeiten vor .NET waren API-Funktionen der Schlüssel zu zahllosen Zusatzfunktionen, die in VB6 (oder auch in anderen Programmiersprachen) nicht direkt zur Verfügung standen. Es gab nur wenige professionelle VB6-Programme, die ohne den Aufruf irgendwelcher API-Funktionen realisiert werden konnten. Mit .NET hat sich das grundlegend geändert: Fast alle Betriebssystemfunktionen können nun komfortabel über .NET-Bibliotheken genutzt werden. Damit hat der direkte Aufruf von API-Funktionen ganz wesentlich an Bedeutung verloren. Leider sind die .NET-Bibliotheken aber noch unvollständig, und die eine oder andere Betriebssystemfunktion kann nach wie vor nur als API-Funktionen genutzt werden. Außerdem gibt es unzählige VB6Programme, bei deren .NET-Portierung es oft einfacher ist, eine API-Funktionen wie bisher aufzurufen, als nach der äquivalenten .NET-Klasse zu suchen. Dieser Abschnitt vermittelt nur einen ersten Einstieg in das Thema API-Funktionen. Weitere Informationen finden Sie in der Hilfe, wenn Sie nach Verwenden nicht verwalteter DLL-Funktionen bzw. nach Exemplarische Vorgehensweise: Aufrufen von Windows-APIs suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/cpguide/html/cpconconsumingunmanageddllfunctions.htm ms-help://MS.VSCC/MS.MSDNVS.1031/vbcn7/html/vaconCallingWindowsAPIs.htm
VERWEIS
Eine Fülle brauchbarer API-Informationen (allerdings für VB6) finden Sie hier im Internet: http://www.allapi.net/
Gute API-Bücher für VB-Programmierer gibt es momentan ebenfalls nur für VB6. Besonders empfehlenswert sind Hardcore Visual Basic von Bruce McKinney und Visual Basic Programmer's Guide to the Win32 API von Dan Appleman. Ob es je API-Bücher für VB.NET geben wird, ist wegen der sinkenden Bedeutung der API-Funktionen eher zweifelhaft. Einen wie immer fundierten Einstieg bietet Dan Appleman in seinem Buch Moving to VB.NET (immerhin 30 Seiten zu APIFunktionen, mit einigen ziemlich fortgeschrittenen Beispielen). Viele Nebenaspekte, die im Zusammenhang mit API-Funktionen zu beachten sind, werden auch in den Wälzern .NET and COM: The Complete Interoperability Guide von Adam Nathan und COM and .NET Interoperability von Andrew Troelsen behandelt.
12.7 API-Funktionen verwenden (Declare)
547
API-Funktionen verwenden Bevor eine API-Funktion in einem VB.NET-Programm aufgerufen werden kann, muss sie mit all ihren Parametern durch Declare deklariert werden. Dabei treten in der Praxis einige Probleme auf: •
Die Syntax von Declare ist relativ unübersichtlich – nicht zuletzt deshalb, weil die Syntax seit ihrer Einführung (Windows 3.1!) immer wieder erweitert werden musste, um der Weiterentwicklung der Betriebssystemfunktionen Rechnung zu tragen.
•
Es ist nicht immer einfach, die richtige API-Funktion für eine konkrete Aufgabe in der Dokumentation zu finden. Alle API-Funktionen sind in der MSDN-Hilfe dokumentiert, die im Rahmen von VS.NET installiert wird. Ein erster Startpunkt für die Suche sollte das Hilfeverzeichnis MSDN-LIBRARY|WINDOWS-ENTWICKLUNG|WIN32-API sein.
•
Wenn das gelungen ist, muss die Parameterliste an die Syntax bzw. die Merkmale von VB.NET angepasst werden. (API-Funktionen sind eigentlich für den Aufruf durch die Programmiersprache C vorgesehen und sind entsprechend dokumentiert.) Besonders schwierig ist es, C-Datenstrukturen in VB.NET nachzubilden. Es gibt zahllose Attribute, mit denen der VB.NET-Compiler angewiesen werden muss, bestimmte Optimierungen nicht durchzuführen, die Reihenfolge von Strukturelementen im Speicher nicht zu verändern etc.
Sobald die Declare-Anweisung richtig durchgeführt ist, können Sie die API-Funktion wie eine gewöhnliche Prozedur oder Methode aufrufen. Falls dabei ein Fehler auftritt, können Sie Err.LastDLLError Informationen über den Fehler entnehmen. Beachten Sie, dass API-Funktionen als unmanaged code ausgeführt werden müssen. (Die API-Funktionen sind ja Teil von Bibliotheken, die außerhalb der .NET-Welt stehen.) Deswegen kommen die diversen .NET-Sicherheitsmechanismen nicht zum Tragen. Und aus diesem Grund dürfen API-Funktionen nur dann aufgerufen werden, wenn Ihr Programm in der höchsten .NET-Sicherheitsstufe ausgeführt wird.
Einführungsbeispiel Bevor die Syntax von Declare etwas ausführlicher beschrieben wird, gibt das folgende Beispiel eine erste Vorstellung, wie die Anwendung von API-Funktionen aussieht. Das Beispiel ermittelt den Pfad des Windows-Verzeichnisses (den Sie natürlich viel bequemer mit Environ("windir") ermitteln können). Dazu wird die Funktion GetEnvironmentVariable aus der Kernel32-Bibliothek eingesetzt. An diese Funktion muss im ersten Parameter der Name der gesuchten Variable übergeben werden. An den zweiten Parameter wird die StringVariable übergeben, in die das Ergebnis eingetragen wird. Der dritte Parameter gibt die maximal zulässige Länge der Zeichenkette an. Die Ergebnisvariable winpath muss vor dem Aufruf der Funktion mit Leerzeichen initialisiert werden. Nach dem Aufruf muss die Zeichenkette auf die tatsächliche Länge verkürzt werden. (GetEnvironmentVariable beendet die Zeichenkette mit einem 0-Zeichen, das von VB.NET aber nicht als Ende einer Zeichenkette erkannt wird.)
548
12 Spezialthemen
' Beispiel spezial\api-intro Private Declare Auto Function GetEnvironmentVariable Lib "kernel32" _ (ByVal lpName As String, ByVal lpBuffer As String, _ ByVal nSize As Integer) As Integer Sub Main() Dim pos As Integer Dim winpath As String = Space(256) GetEnvironmentVariable("windir", winpath, 255) winpath = Left(winpath, InStr(winpath, Chr(0)) - 1) Console.WriteLine("Windows-Verzeichnis: " + winpath) Console.WriteLine("Return drücken") Console.ReadLine() End Sub
Declare-Syntax Die Kurzfassung der Syntax sieht folgendermaßen aus (je nachdem, ob die API-Funktion einen Rückgabewert liedert oder nicht): Declare [Auto|Ansi|Unicode] Sub name Lib "lib.dll" _ [Alias "name2"] (parameterliste) Declare [Auto|Ansi|Unicode] Function name Lib "lib.dll" _ [Alias "name2"] (parameterliste) As datentyp Declare-Anweisungen müssen in Modulen oder Klassen durchgeführt werden (nicht auf Codedateiebene, nicht innerhalb von Prozeduren). Declare kann ein Gültigkeitsbezeichner vorangestellt werden (z.B. Public, Private etc.).
In den Zeiten von Windows 3.1 und Windows 95 verarbeiteten alle API-Funktionen ausschließlich ANSI-Zeichenketten. Bei neueren Versionen vollzog Microsoft dann den Schritt hin zu Unicode. Aus Kompatibilitätsgründen musste der ANSI-Zeichensatz aber weiter unterstützt werden. Deswegen gibt es von vielen API-Funktionen zwei Versionen, deren Name sich durch den Endbuchstaben unterscheidet: nameA verarbeitet ANSI-Zeichenketten, nameW verarbeitet Unicode-Zeichenketten. (W steht für wide und bezieht sich darauf, dass für die Darstellung jedes Zeichens zwei Byte erforderlich sind.) Die optionalen Schlüsselwörter Ansi (gilt per Default), Auto und Unicode geben an, welche Version der API-Funktion aufgerufen werden soll und ob VB.NET beim Aufruf automatisch eine Umwandlung zwischen Unicode (dem Defaultformat aller VB.NET-Zeichenketten) und ANSI durchführt. •
Ansi: Es wird die ANSI-Version der API-Funktion aufgerufen. An den Funktionsnamen wird bei Bedarf automatisch der Buchstabe -A angefügt. VB.NET-Zeichenketten werden vor dem Aufruf von Unicode zu ANSI konvertiert, nach dem Aufruf zurück zu Unicode. Ansi gilt aus Kompatibilitätsgründen zu VB6 als Defaulteinstellung.
12.7 API-Funktionen verwenden (Declare)
•
549
Unicode: Es wird die Unicode-Version der API-Funktion aufgerufen. An den Funktionsnamen wird der Buchstabe -W angefügt. Eine Konvertierung der Zeichenketten ist
nicht erforderlich. •
Auto: Es wird je nach Betriebssystem die ANSI-Version (Windows 98/ME) oder die
Unicode-Version verwendet (Windows NT/2000/XP). Das ist insofern praktisch, als die Unicode-Variante zwar effizienter ist, aber manche API-Funktionen unter Windows 98/ME nach wie vor nur in der ANSI-Variante zur Verfügung stehen. Lib gibt den Namen der Bibliothek an, in der sich die DLL-Funktion befindet (z.B. "kernel32.dll"). Die Bibliothek wird im Windows-Verzeichnis, im Windows-Systemverzeichnis und in dem Verzeichnis gesucht, in dem sich die gerade ausgeführte *.exe-Datei befindet.
Mit dem optionalen Alias-Schlüsselwort können Sie den tatsächlichen API-Funktionsnamen angeben. Damit können Sie Namenskonflikte vermeiden (wenn die API-Funktion denselben Namen wie eine bereits vorhandene VB.NET-Funktion hat oder wenn der APIFunktionsname unter VB.NET nicht gültig ist). Die folgende Zeile bewirkt, dass durch den VB.NET-Code abc(...) die API-Funktion GetEnvironmentVariableA aufgerufen wird. Declare Sub abc Lib "kernel32" Alias GetEnvironmentVariableA (...)
Parameterübergabe Die Angabe der Parameterliste erfolgt in derselben Syntax wie bei der Deklaration von VB.NET-Prozeduren, -Methoden oder -Funktionen. Das Kunststück besteht aber darin, die richtige Entsprechung zwischen den Parametern in C-Notation (laut API-Dokumentation) und der VB.NET-Syntax zu finden. Generell müssen Parameter meistens als Wertparameter übergeben werden (also mit ByVal deklariert werden). Nur wenn die API-Funktionen einen Zeiger auf eine Zahl oder auf ein einzelnes Zeichen erwarten muss ByRef verwendet werden. Achtung, Zeichenketten werden immer mit ByVal übergeben! Zahlen: Die Übergabe von Zahlen bereitet selten Probleme. Die folgende Tabelle gibt einige wichtige C- bzw. API-Datentypen und ihre VB.NET-Entsprechung an. C / Win32API
VB.NET
BYTE
ByVal Byte
short, SHORT, WORD
ByVal Short
USHORT
ByVal UInt16 oder ByVal Short
int, long, INT, LONG, DWORD
ByVal Integer
ULONG
ByVal UInt32 oder ByVal Integer
float
ByVal Single
double
ByVal Double
LPSHORT, LPWORD etc. (LP steht für long pointer, daher ByRef!)
ByRef datentyp oder ByVal IntPtr
550
12 Spezialthemen
C / Win32API
VB.NET
short *, word *, int * etc.
ByRef datentyp oder ByVal IntPtr
(* meint in C für einen Zeiger) Zeiger: Viele API-Funktionen erwarten Zeiger (pointer, handle) auf betriebssysteminterne Datenstrukturen (z.B. auf einen so genannten device context, der ein Grafikobjekt beschreibt). Grundsätzlich handelt es sich dabei um Integer-Daten. Wenn der Zeiger mit einer .NET-Methode ermittelt wird, die IntPtr als Ergebnis zurückgibt, muss IntPtr auch bei der Deklaration verwendet werden. (Ein entsprechendes Beispiel finden Sie am Ende dieses Abschnitts.) Zeichenketten: API-Funktionen erwarten 0-terminierte Zeichenketten und geben auch solche Zeichenketten zurück. Bei der Übergabe von Zeichenketten an API-Funktionen ist das kein Problem – VB.NET kümmert sich darum, dass die API-Funktionen die Zeichenketten so erhalten, dass sie damit umgehen können. Bei der Rückgabe von Zeichenketten ist aber Vorsicht geboten. Erstens muss die StringVariable schon vor dem Aufruf eine Zeichenkette enthalten, die so groß ist, dass die größtmögliche Ergebniszeichenkette der API-Funktion darin gespeichert werden kann. Die APIFunktion überschreibt die vorhandene Zeichenkette durch die zurückgegebenen Daten. (Wenn die vorgegebene Zeichenkette zu kurz ist, schneidet VB.NET das API-Ergebnis ohne Fehlermeldung ab.) Zweitens müssen Sie bei der Weiterverarbeitung der Zeichenkette die C-typische 0-Terminierung beachten. Das bedeutet, dass das Ende einer Zeichenkette durch ein 0-Zeichen ausgedrückt wird. Wenn Sie also eine Zeichenkette vor dem Aufruf mit 256 Leerzeichen gefüllt haben (s = Space(256)) und die API-Funktion dorthin eine Ergebniszeichenkette von nur 20 Zeichen speichert, liefert Len(s) nach wie vor 256! Das liegt daran, dass für VB.NET ein 0-Zeichen wie jedes andere Zeichen gilt. Mit der folgenden Anweisung (die bereits im Einführungsbeispiel zur Anwendung gekommen ist) wandeln Sie die API-Zeichenkette in eine unter VB.NET übliche Zeichenkette um. s = Left(s, InStr(s, Chr(0)) - 1)
Wenn Sie häufig API-Funktionen aufrufen, bietet es sich an, eine kleine VB.NET-Funktion zur Zeichenkettenkonvertierung einzusetzen. s = C2VB(s) Function C2VB(ByVal s As String) As String Dim pos As Integer pos = InStr(s, Chr(0)) If pos = 0 Then Return "" Return Left(s, pos - 1) End Function
12.7 API-Funktionen verwenden (Declare)
551
Datenstrukturen: Wenn API-Funktionen als Parameter eine Datenstruktur erwarten, besteht das Problem darin, eine äquivalente Datenstruktur mit VB.NET nachzubilden. Dabei ist entscheidend, dass sich die einzelnen Elemente der Datenstruktur exakt an der Position im Speicher befinden, an der die API-Funktion – vertrauend auf die Regeln der Programmiersprache C – diese erwartet. In VB.NET müssen Sie Datenstrukturen durch Structure – End Structure bilden. Dabei entscheidet normalerweise der VB.NET-Compiler, wie er die Daten im Speicher organisiert. Um ein C-konformes Speicherlayout zu erzwingen, müssen Sie das Attribut StructLayout mit den Parametern LayoutKind.Sequential und Pack:=1 verwenden. Das bedeutet, dass die Elemente der Struktur in der von Ihnen angegebenen Reihenfolge im Speicher abgelegt werden und dass so genanntes single-byte-packing verwendet wird. Das bedeutet, dass Mehrbyteparameter an jedem beliebigen Byte beginnen dürfen. (Normalerweise werden Short-Daten nur an geraden Adressen und Integer-Daten nur an Adressen abgelegt, die ein Vielfaches von Vier betragen.) Das Attribut StructLayout ist im Namensraum Runtime.InteropServices definiert. Imports System.Runtime.InteropServices _ Structure structname Dim hWnd As Integer Dim wFunc As Integer ... End Structure
VERWEIS
Ein weiteres Problem besteht darin, dass manche API-Datenstrukturen Zeichenketten oder Felder mit fixer Länge verwenden. Beides ist in VB.NET in Strukturen nicht vorgesehen. Das Problem kann umgangen werden, indem einzelne Elemente der Struktur mit dem Attribut MarshalAs deklariert werden, das ebenfalls im Namensraum Runtime.InteropServices definiert ist. Ein Beispiel für die Deklaration einer Datenstruktur mit dem Attribut StructLayout finden Sie in Abschnitt 10.3.3.
Konstanten: Viele API-Funktionen erwarten in einzelnen Parametern numerische Konstanten. Das Problem liegt hier weder bei der Deklaration des Parameters noch bei der Übergabe der Werte an die Funktion, sondern darin, den Wert der Konstante festzustellen. In der Dokumentation ist nämlich normalerweise nur der Name, nicht aber der Wert der Konstanten zu finden. Um die Konstanten zu ermitteln, können Sie entweder den im nächsten Abschnitt beschriebenen API-Viewer einsetzen, oder Sie müssen die entsprechende C-Header-Datei suchen, in der die Konstanten definiert sind. Beispielsweise finden Sie die Konstanten der GDI32-Bibliothek in der Datei Programme\Microsoft Visual Studio .NET\Vc7\PlatformSDK\Include\WinGDI.h. (Diese Datei befindet sich nur dann auf Ihrem Rechner, wenn Sie bei der Visual-Studio-Installation die Visual-C++-Klassenbibliotheken mitinstalliert haben!)
552
12 Spezialthemen
API-Deklaration mit DllImportAttribut Als Variante zu Declare können API-Funktionen auch wie gewöhnliche Funktionen mit Function – End Function deklariert werden, wenn das Attribut Runtime.InteropServices.DllImport vorangestellt wird. Innerhalb der Funktion braucht kein Code angegeben zu werden, weil ja ohnedies eine externe API-Funktion aufgerufen wird. Die folgenden Zeilen deklarieren nochmals die aus dem Einführungsbeispiel schon vertraute Funktion GetEnvironmentVariable. Im ersten Parameter von DllImport wird der Bibliotheksname angegeben. EntryPoint gibt den internen Funktionsnamen an. Das angehängte W bedeutet, dass die Unicode-Version (wide) der Funktion verwendet werden soll. Entsprechend gibt CharSet an, dass Zeichenketten beim Transport zwischen VB.NET und der APIFunktion im Unicode-Zeichensatz belassen werden sollen. Die DllImport-Schreibweise bietet im Wesentlichen dieselben Funktionen wie Declare, die Syntax ist aber noch unübersichtlicher. Insofern spricht nichts dagegen, bei Declare zu bleiben. Die Kenntnis von DllImport ist dennoch wertvoll, wenn Sie C#-Code lesen: Dort gibt es keine Declare-Anweisung, d.h., alle API-Funktionen müssen via DllImport definiert werden. Imports System.Runtime.InteropServices _ Private Function GetEnvironmentVariable(ByVal lpName As String, _ ByVal lpBuffer As String, ByVal nSize As Integer) As Integer
TIPP
End Function
Wenn Ihnen die Beschreibung mancher Declare-Syntaxelemente in der Online-Hilfe vage erscheint, suchen Sie in der Hilfe das äquivalente DllImport-Schlüsselwort! Sie werden mit einer deutlich präziseren Beschreibung belohnt.
12.7.2 API-Viewer Der vorige Abschnitt hat wahrscheinlich klar gemacht, dass es mühsam sein kann, die Deklaration für API-Funktionen, Konstanten und Strukturen zusammenzustellen. Mit VB6 wurde deswegen das Programm API-Viewer mitgeliefert, das Zugriff auf eine Datenbank vordefinierter Declare-, Const- und Structure-Anweisungen gab. Mit VB.NET ist dieses Programm aber verschwunden. Pramod Kumar Singh hat sich die Mühe gemacht, eine VB.NET- und C#-taugliche Version dieses Programms zu entwickeln. Sie finden das Programm (samt VB.NET-Quelltext) im Internet, wenn Sie nach API Viewer for VB.NET suchen. Zuletzt wurde das Programm hier zum Download angeboten: http://www.freevbcode.com/ShowCode.Asp?ID=3639
Nach dem Programmstart müssen Sie die Datei win32api.txt öffnen. Diese von Microsoft zusammengestellte Datei enthält Declare-Anweisungen und Konstantendefinitionen in der
12.7 API-Funktionen verwenden (Declare)
553
Syntax von VB6. Die Datei wird mit dem API-Viewer normalerweise nicht mitgeliefert. Wenn Sie VB6 noch auf Ihrem Rechner installiert haben, finden Sie diese Datei im Verzeichnis Programme\Microsoft Visual Studio\Common\Tools\Winapi. Eine aktualisierte Version befindet sich laut News-Gruppenberichten im Microsoft-Platform-SDK-Paket. Sie finden dieses 900 MByte große Paket im Download-Bereich von www.microsoft.com. Ich habe den Riesen-Download allerdings nicht durchgeführt und kann nicht garantieren, dass win32api.txt wirklich Teil des Pakets ist.
TIPP
Egal welche Version von win32api.txt Sie nun verwenden – nach dem Laden der Datei können Sie im API-Viewer nach Funktionsdeklarationen, Konstanten und Datenstrukturen suchen. Das Programm wandelt die Deklarationen in die VB.NET-Syntax um und Sie können den Code dann über die Zwischenablage in Ihr Programm kopieren. Sie finden den API Viewer for VB.NET (Stand Juni 2002) und die Datei win32api.txt (die mit VB6 gelieferte Version, Stand Juni 1998) auch auf der beiliegenden CD im Verzeichnis spezial\win32api.
Abbildung 12.9: Der API-Viewer
HINWEIS
554
12 Spezialthemen
Erwarten Sie sich von dem Programm keine Wunder! Abbildung 12.9 zeigt beispielsweise, dass der Parameter hdc der Funktion GetDeviceCaps als Integer deklariert ist. Beim folgenden Beispielprogramm wird sich herausstellen, dass dieser Parameter in Wirklichkeit als IntPtr deklariert werden muss. Derartige Fehler gibt es leider zuhauf. (Die Fehler werden nicht durch das Programm API-Viewer verursacht, sondern durch die zugrunde liegende Datei win32api.txt.) Ein weiteres Problem besteht darin, dass viele moderne API-Funktionen (die erst nach Windows 95/NT eingeführt wurden) in der Datei ganz fehlen. Trotz dieser Mängel ist das Programm in vielen Fällen eine wertvolle Hilfe.
VERWEIS
12.7.3 Beispiele Ein weiteres API-Beispiel finden Sie in Abschnitt 10.3.3. Dort wird die Funktion SHFileOperation eingesetzt, um eine Datei in den Papierkorb zu löschen.
Bildschirmauflösung ermitteln Das Windows-Programm api-resolution verwendet die Funktion GetDeviceCaps aus der Grafikbibliothek GDI32, um die aktuelle Bildschirmauflösung in Pixeln zu ermitteln. (Diese Information könnte natürlich viel einfacher mit den Klassen Windows.Forms.Screen oder Windows.Forms.SystemInformation ermittelt werden, siehe Abschnitt 15.2.8.) An diese Funktion muss im ersten Parameter ein so genannter Device Context übergeben werden. Dabei handelt es sich um einen Zeiger auf ein Objekt, das das Fenster beschreibt. Im zweiten Parameter muss eine Nummer übergeben werden, die angibt, welche Information ermittelt werden soll. In der Online-Dokumentation ist die Syntax dieser Funktion so dargestellt: int GetDeviceCaps(HDC hdc, int nIndex)
Mit Declare wird diese Funktion nun für den Aufruf unter VB.NET deklariert. Dabei wird für den Parameter hdc der Datentyp IntPtr verwendet. Das ist notwendig, weil die etwas weiter unten beschriebene Methode GetHdc ein Objekt des Typs IntPtr liefert. Aus Sicht der API-Funktion gibt es keinen Unterschied zwischen Integer und IntPtr – beides sind einfach 32-Bit-Zahlen. Der VB.NET-Compiler unterscheidet aber sehr wohl zwischen den beiden Datentypen und vermeidet so eine irrtümliche Fehlanwendung von Adressen. (Generell müssen API-Parameter, die als handle bezeichnet werden, häufig mit IntPtr deklariert werden, wenn der zu übergebende Zeiger vorher mit .NET-Funktionen ermittelt wird.)
12.7 API-Funktionen verwenden (Declare)
555
' Beispiel spezial\api-resolution Private Declare Function GetDeviceCaps Lib "gdi32.dll" _ (ByVal hdc As IntPtr, ByVal nIndex As Integer) As Integer Private Const HORZRES = 8 'Horizontal width in pixels Private Const VERTRES = 10 'Vertical width in pixels
Beim Aufruf der Funktion besteht das größte Problem darin, den erforderlichen HDC-Parameter anzugeben. Dieser steht in Windows.Forms-Programmen nämlich (anders als in VB6) nicht mehr unmittelbar zur Verfügung. Stattdessen muss mit CreateGraphics ein Graphics-Objekt für das aktuelle Fenster erzeugt werden. Anschließend kann mit der Methode GetHdc der interne handle ermittelt werden. Dieser Zeiger muss später mit ReleaseHdc wieder freigegeben werden. Ebenso muss das Graphics-Objekt mit Dispose wieder freigegeben werden. Private Sub Form1_Load(...) Handles MyBase.Load Dim width, height As Integer Dim gr As Graphics = CreateGraphics() Dim hdc As IntPtr = gr.GetHdc() width = GetDeviceCaps(hdc, HORZRES) height = GetDeviceCaps(hdc, VERTRES) gr.ReleaseHdc(hdc) gr.Dispose() Label1.Text = "Bildschirmauflösung: " + _ width.ToString + "*" + height.ToString + " Pixel" End Sub
Abbildung 12.10: Die Bildschirmauflösung wird mit einer API-Funktion ermittelt
Größe einer komprimierten Datei ermitteln Windows NT/2000/XP bietet bei Verwendung des NTFS-Dateisystems die Möglichkeit, Dateien zu komprimieren. Derartige Dateien benötigen dann auf der Festplatte weniger Platz. Die Eigenschaft Length der Klasse IO.FileInfo gibt bei solchen Dateien immer die unkomprimierte Größe an. (Das ist durchaus sinnvoll, weil die Datei beim Lesen ja automatisch dekomprimiert wird.) Wenn Sie nun aber wissen möchten, wie viel Platz die komprimierte Datei auf der Festplatte beansprucht, müssen Sie die API-Funktion GetCompressedSize einsetzen. Das folgende Beispielprogramm demonstriert die Anwendung dieser Funktion (siehe Abbildung 12.11).
556
12 Spezialthemen
Beachten Sie, dass GetCompressedSize nur unter den genannten Betriebssystemen zur Verfügung steht!
Abbildung 12.11: Das Beispielprogramm zeigt die tatsächliche und die komprimierte Größe einer Bitmap-Datei
Die Funktion GetCompressedFileSize befindet sich in der Bibliothek kernel32. Sie erwartet als ersten Parameter den Dateinamen. Das Ergebnis wird in zwei Teilen zurückgegeben: Der Rückgabewert enthält die ersten 32 Bit der Dateigröße. (Das reicht für Dateien bis zu 4 GByte.) Die zweiten 32 Bit werden in die im zweiten Parameter übergebene Integer-Variable geschrieben. Beachten Sie, dass der zweite Parameter als ByRef deklariert werden muss. Es wird hier also ein Zeiger auf einen Integer-Wert übergeben. (Der API-Viewer liefert hier fälschlich ByVal.) ' Beispiel spezial\api-getcompressedsize Private Declare Auto Function GetCompressedFileSize Lib "kernel32" _ (ByVal lpFileName As String, ByRef lpFileSizeHigh As Integer) _ As Integer Private Sub Button1_Click(...) Handles Button1.Click Dim fname As String Dim fi As IO.FileInfo Dim sizelow, sizehigh As Integer Dim sizetotal As Long If OpenFileDialog1.ShowDialog = DialogResult.OK Then fname = OpenFileDialog1.FileName fi = New IO.FileInfo(fname) Label1.Text = "Datei: " + fname Label2.Text = "Größe: " + FormatNumber(fi.Length, 0) + " Byte" sizelow = GetCompressedFileSize(fname, sizehigh) sizetotal = sizelow + sizehigh * CLng(2 ^ 32) Label3.Text = "Komprimierte Größe: " + _ FormatNumber(sizetotal, 0) + " Byte" End If End Sub
Teil IV
Windows-Programmierung
13 Windows.Forms – Einführung Windows.Forms ist der Teil der .NET-Bibliothek, der für die Anzeige und Verwaltung von Fenstern (Dialogen, Formularen) und den darin eingebetteten Steuerelementen zuständig ist. Jedes VB.NET-Programm, das mit einer Windows-Benutzeroberfläche ausgestattet werden soll, basiert auf Windows.Forms.
Dieses Kapitel gibt eine erste Einführung in die Erstellung eigener Formulare sowie einen Überblick über die dabei zur Anwendung kommenden Bibliotheken, Klassen und Steuerelemente. Außerdem beschreibt das Kapitel die Anwendung der Entwicklungsumgebung zum Design von Formularen. Die beiden folgenden Kapitel geben dann einen Überblick über die wichtigsten zur Verfügung stehenden Steuerelemente sowie eine Menge Details und Interna zur Gestaltung eigener Benutzeroberflächen (Verwaltung mehrerer Fenster, Menüs und Symbolleisten etc.) 13.1 13.2 13.3
Einführung Elementare Programmiertechniken Windows.Forms-Klassenhierarchie
560 569 573
560
13.1
13 Windows.Forms – Einführung
Einführung
Im Mittelpunkt dieses und der folgenden beiden Kapitel steht die Bibliothek Systems.Windows.Forms. Diese Bibliothek stellt im gleichnamigen Namensraum unzählige Klassen zur Verwaltung von Fenstern und Steuerelementen zur Verfügung. (Die Bibliothek enthält noch einige weitere Namensräume, die aber von untergeordneter Bedeutung sind.) Wenn Sie ein neues Projekt des Typs WINDOWS-ANWENDUNG entwickeln, wird automatisch ein Verweis auf diese Bibliothek eingerichet. Außerdem gilt System.Windows.Forms als Default-Import, weswegen Klassen aus diesem Namensraum im Code verwendet werden können, ohne dass jedes Mal Windows.Forms vorangestellt werden muss.
13.1.1 Kleines Glossar Die drei Begriffe Fenster, Formulare und Dialoge sind – zumindest was die Interna angeht – gleichbedeutend. In jedem Fall ist das am Bildschirm sichtbare Fenster von der Klasse Form abgeleitet – ganz egal, ob es nun als Dialog, als Eingabeformular oder für einen beliebigen anderen Zweck verwendet wird. Eine Sonderstellung nimmt manchmal der Begriff Dialog ein: Ein Fenster wird üblicherweise dann als Dialog bezeichnet, wenn es den Rest des Programms blockiert. (Das heißt, dass die Arbeit im Hauptprogramm erst dann wieder fortgesetzt werden kann, wenn die Eingabe im Dialog beendet ist.) Aber auch hierzu gibt es Ausnahmen: Beispielsweise darf der Dialog zum SUCHEN UND ERSETZEN in den meisten Programmen geöffnet bleiben, während Sie im Hauptprogramm weiterarbeiten. (Das gilt z.B. für Microsoft Word oder für die VB.NET-Entwicklungsumgebung.) Exakter ist in solchen Fällen die Bezeichnung modaler oder nichtmodaler Dialog. Modal bedeutet, dass der Dialog das Hauptprogramm blockiert. Nichtmodal bedeutet, dass der Dialog als gleichberechtigtes zweites Fenster agiert. Steuerelemente sind die Bedienelemente in einem Fenster. Zu Steuerelementen zählen z.B. Buttons, Kontrollkästchen, Optionsfelder, Textfelder, Schiebebalken etc. Während des Programmentwurfs steht ein komfortabler Editor (Designer) zur Verfügung, um Steuerelemente auf einem Fenster zu platzieren. (Sie können neue Steuerelemente aber auch dynamisch im laufenden Programm durch ein paar Zeilen zusätzlichen Code erzeugen, wenn Sie das möchten.) Was Eigenschaften und Methoden sind, wissen Sie ja schon. Mit Eigenschaften können Sie Merkmale eines Objekts lesen und zum Teil verändern. Mit Methoden können Sie das Objekt bearbeiten. Da sowohl ein Fenster als auch die darin enthaltenen Steuerelemente intern (natürlich) Objekte sind, können auch sie durch zahllose Eigenschaften und Methoden gesteuert werden. Die einzige Neuerung im Zusammenhang mit Steuerelementen und Formularen besteht darin, dass Sie in vielen Fällen ein sofortiges visuelles Feedback bekommen: Wenn Sie also die Eigenschaft BackColor eines Fensters verändern, ändert sich unmittelbar auch die Farbe des Fensterhintergrunds. Neu ist auch, dass eine Menge (aber nicht alle) Eigenschaften während der Programmentwicklung sehr komfortabel über das Fenster EIGENSCHAFTEN voreingestellt werden können.
13.1 Einführung
561
VERWEIS
Ereignisse sollten Sie ebenfalls schon kennen (siehe Kapitel 7), aber möglicherweise haben Sie noch wenig praktische Erfahrungen damit gemacht. Das wird sich jetzt rasch ändern, denn der gesamte Programmfluss eines Windows-Programms ist durch Ereignisse gesteuert. Wenn beispielsweise ein Anwender Ihres Programms einen Button anklickt, wird für diesen Button ein Click-Ereignis ausgelöst. In einer Ereignisprozedur können Sie darauf reagieren. (Die Vorgehensweise wird im folgenden Beispiel demonstriert.) In diesem Kapitel werden Steuerelemente einfach eingesetzt, ohne detailliertes Hintergrundwissen über deren Natur, Eigenschaften, Methoden etc. zu vermitteln. Diese Informationen werden aber im folgenden Kapitel nachgereicht.
13.1.2 Einführungsbeispiel Ziel des Einführungsbeispiels ist es, einen minimalistischen Texteditor zu programmieren. Das Programm besteht aus einem Fenster, das ein Textfeld und zwei Buttons enthält. Mit den beiden Buttons kann der Text aus einer ANSI-Datei geladen bzw. wieder gespeichert werden.
Steuerelemente in das Formular einfügen Der Programmentwurf beginnt damit, dass Sie ein neues VB.NET-Projekt des Typs WINDOWS-ANWENDUNG starten. Nun sollten Sie die Entwicklungsumgebung so einstellen, dass sowohl das Formular im Design-Modus als auch die Fenster bzw. Registrierkarten TOOLBOX und EIGENSCHAFTEN sichtbar sind (siehe auch Abbildung 13.1). Zum Einfügen der Steuerelemente markieren Sie das gewünschte Steuerelement zuerst in der Toolbox. Anschließend zeichnen Sie mit der Maus einen Rahmen an der Position im Fenster, an der das Steuerelement eingefügt werden soll. Das Steuerelement erscheint dort, sobald Sie die Maustaste loslassen. (Eine alternative Vorgehensweise besteht darin, das Steuerelement durch einen Doppelklick in der Toolbox einzufügen. Die Entwicklungsumgebung entscheidet dann selbst über Ort und Größe des Steuerelements. Anschließend verschieben Sie das Steuerelement an die gewünschte Stelle.) Für das hier vorgestellte Beispielprogramm benötigen Sie fünf Steuerelemente: zwei Buttons, ein Textfeld (TextBox) sowie je ein OpenFileDialog- und SaveFileDialog-Steuerelement. Die beiden letzten Steuerelemente stellen insofern einen Spezialfall dar, als sie nicht direkt im Formular angezeigt werden, sondern in einem eigenen Bereich unterhalb des Formulars. (Dieser Bereich ist für unsichtbare Steuerelemente vorgesehen.)
Einstellung der Eigenschaften Der nächste Schritt besteht darin, die Eigenschaften der Steuerelemente sowie des Formulars im Fenster EIGENSCHAFTEN einzustellen. Die im EIGENSCHAFTEN-Fenster angezeigten Einstellungen gelten für das im Formulardesign gerade aktive (angeklickte) Steuerelement.
562
13 Windows.Forms – Einführung
Sie können dieses Steuerelement auch mit dem Listenfeld des EIGENSCHAFTEN-Fensters auswählen. Die Eigenschaften bestimmen das Aussehen und Verhalten der Steuerelemente. Welche Eigenschaften zur Auswahl stehen, hängt vom Steuerelement ab. (Eine Menge Eigenschaften werden im nächsten Kapitel beschrieben. Dort finden Sie auch einen Überblick über gemeinsame Eigenschaften, die fast alle Steuerelemente besitzen.) Für das Beispielprogramm wurden folgende Eigenschaften verändert: •
Für das Fenster (Form-Objekt): Text = "Einfacher Texteditor"
•
Für die beiden Buttons: Text="ANSI-Datei laden" bzw. Text="ANSI-Datei speichern"
•
Für das Textfeld: Text="" (damit das Steuerelement beim Programmstart leer ist) MultiLine=True (damit im Textfeld mehrere Zeilen angezeigt werden können) WordWrap=False (damit lange Zeilen nicht auf mehrere Zeilen verteilt werden) ScrollBars=Both (damit bei langen Texten Schiebebalken angezeigt werden) AcceptTabs=True (damit mit Tab ein Tabulatorzeichen eingegeben werden kann) Anchor=Top, Bottom, Left, Right (damit das Steuerelement sich automatisch an die Größe des Fensters anpasst)
Abbildung 13.1: Entwurf des Fensters des Texteditors in der Entwicklungsumgebung
13.1 Einführung
563
Die beiden letzten Eigenschaften bedürfen einer etwas ausführlicheren Erklärung: Tab dient normalerweise dazu, den Eingabefokus von einem Steuerelement zum nächsten zu bewegen. Das macht es aber unmöglich, in einem Textfeld ein Tabulatorzeichen einzugeben. AcceptTabs behebt diesen Mangel.
HINWEIS
Die Anchor-Eigenschaft gibt an, welche Kanten des Steuerelements mit den Kanten des Fensters verbunden sind. Normalerweise ist das nur für Top und Left der Fall. Das bedeutet, dass das Steuerelement bei einer Größenänderung Position und Größe behält. Wenn auch die rechte und die untere Kante verankert werden, dann bleibt der Abstand zwischen dem Steuerelement und dem rechten bzw. unteren Fensterrand konstant. Dazu wird das Steuerelement automatisch mit dem Fenster vergrößert bzw. verkeinert. Bevor Sie die Anchor-Eigenschaft einstellen, müssen Sie das Textfeld relativ zur Fenstergröße im Formular-Designer richtig positionieren. Normalerweise sollten Sie den Steuerelementen aussagekräftige Namen geben. Dazu ändern Sie die Name-Eigenschaft des jeweiligen Steuerelements. Beispielsweise wären für die beiden Buttons die Bezeichnungen ButtonOK und ButtonCancel aussagekräftiger (statt der Defaultbezeichnungen Button1 und Button2). Beachten Sie, dass Sie die Steuerelemente umbenennen müssen, bevor Sie mit der Codeeingabe beginnen! (Nachträgliche Änderungen werden im Code nicht ausgeführt, d.h., Sie müssen dann den Code an den geänderten Namen anpassen.)
Sie können das Programm jetzt bereits starten (siehe Abbildung 13.2) – es erfüllt aber vorerst noch keine sinnvolle Aufgabe. Wenn Sie die beiden Buttons anklicken, passiert nichts. Text können Sie bereits eingeben, aber noch nicht speichern.
Abbildung 13.2: Aussehen des Fensters bei der Ausführung des Programms
Tipps zur effizienten Bedienung des Eigenschaftsfensters •
Im Eigenschaftsfenster sind alle Eigenschaften, die nicht die Defaulteinstellung enthalten, fett hervorgehoben.
•
Die Eigenschaften können im Eigenschaftsfenster wahlweise alphabetisch oder nach Gruppen geordnet dargestellt werden. Zwischen diesen beiden Modi kann mit den Buttons im linken oberen Eck des Eigenschaftsfenster gewechselt werden. Die alphabetische Anordung erleichtert oft die Suche nach einer bestimmten Eigenschaft.
564
13 Windows.Forms – Einführung
•
Wenn Sie eine Einstellung zurücksetzen möchten, klicken Sie die Eigenschaft mit der rechten Maustaste an und führen das Kontextmenükommando RESET aus.
•
Falls Sie die Entwicklungsumgebung so konfiguriert haben, dass das Eigenschaftsfenster normalerweise nicht angezeigt wird (AUTOMATISCH IM HINTERGRUND), dann lohnt es sich während des Formularentwurfs, das Fenster mit der Pinnadel vorübergehend anzudocken.
•
Im Eigenschaftsfenster können Sie auch gemeinsame Eigenschaften mehrerer Steuerelemente verändern. Dazu müssen Sie die Steuerelemente gemeinsam markieren – entweder mit Strg + Mausklick, oder indem Sie mit der Maus einen Rahmen über alle zu markierenden Steuerelemente zeichnen.
•
Wenn Sie mehrere gleichartige Steuerelemente benötigen, ist es oft am einfachsten, zuerst ein Steuerelement einzufügen, dessen Eigenschaften einzustellen und das Steuerelement dann mit KOPIEREN/EINFÜGEN (Strg+C, Strg+V) zu vervielfältigen.
•
Wenn Sie im Eigenschaftsfenster eine Eigenschaft markieren, gelangen Sie mit F1 direkt zum dazugehörenden Hilfetext.
Bei der Ausrichtung mehrerer Steuerelemente (z.B. Einstellung eines gleichmäßigen Abstands) hilft die LAYOUT-Symbolleiste. Wenn Sie diese Symbolleiste wie ich aus Platzgründen per Default nicht anzeigen lassen, vergessen Sie nicht, dass es sie gibt und dass sie durchaus praktisch ist! Beachten Sie auch, dass Sie mehrere Steuerelemente gemeinsam markieren können. Anschließend betreffen Änderungen der Position oder Größe alle Steuerelemente.
Ereignisprozeduren Was dem Programm jetzt noch fehlt, ist der Programmcode zum Laden und Speichern einer Datei. Grundsätzlich wird der Code zu Formularen ereignisorientiert verarbeitet. Die Reaktion auf Benutzereingaben (Mausklicks, Tastatureingaben etc.) sind Ereignisse. Wenn es zu einem Ereignis eine dazugehörende Ereignisprozedur gibt, wird diese automatisch ausgeführt. Es gibt drei Methoden, um das Gerüst einer Ereignisprozedur in den Code einzufügen: •
Am bequemsten und effizientesten ist ein Doppelklick auf das jeweilige Steuerelement im Designmodus. Die Entwicklungsumgebung zeigt dann automatisch den Programmcode zum Formular an und füg dort das Gerüst der Prozedur zum Defaultereignis ein. Besonders gut funktioniert das bei den Buttons: Durch einen Doppelklick wird die Click-Ereignisprozedur eingefügt. (Im Regelfall ist das die einzige Ereignisprozedur eines Buttons, die Sie brauchen.)
•
Wenn Sie eine Prozedur zu einem anderen Ereignis als dem Defaultereignis schreiben möchten, wechseln Sie in das Codefenster zum Formular. (Falls dieses Fenster nicht schon offen ist, können Sie es am schnellsten über das Kontextmenü CODE ANZEIGEN des Designfensters öffnen.)
13.1 Einführung
565
Dort wählen Sie zuerst im linken Listenfeld das gewünschte Steuerelement, dann im rechten Listenfeld den Ereignisnamen aus (siehe Abbildung 13.3). Um eine Ereignisprozedur für das Fenster einzufügen, wählen Sie links den Listeneintrag BASISKLASSENEREIGNISSE aus. •
Natürlich können Sie den Code auch einfach über die Tastatur eingeben. Das ist aber mühsam, weil Sie sowohl die Parameterliste als auch den Ereignistyp (Handles XyEvent) exakt angeben müssen.
Abbildung 13.3: Codegerüst für eine Ereignisprozedur einfügen
Das Codegerüst sieht wie im folgenden Muster aus: Der Name der Prozedur ergibt sich aus dem Steuerelement- und dem Ereignisnamen. Im Anschluss an die Parameterliste gibt das Schlüsselwort Handles an, auf welches Ereignis die Prozedur reagiert. Die Parameterliste sieht immer sehr ähnlich aus: •
sender enthält einen Verweis auf das Steuerelement, für das das Ereignis verarbeitet wird. Im Regelfall ist eine Auswertung von sender nicht erforderlich. sender ist aber
dann praktisch, wenn mehrere Steuerelemente mit einer Ereignisprozedur verbunden werden (siehe Abschnitt 14.11.2). In diesem Fall kann sender mit CType in ein Steuerelementobjekt umgewandelt werden, z.B. so: Dim btn As Button If TypeOf sender Is Button Then btn = CType(sender, Button) ...
•
e enthält ereignisspezifische Daten. Der Objekttyp hängt vom Ereignis ab. Bei Ereignissen ohne ereignisspezifische Daten ist e ein Objekt der Klasse System.EventArgs. In diesem Fall kann e einfach ignoriert werden.
566
13 Windows.Forms – Einführung
Private Sub Button1_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles Button1.Click ... End Sub
Aus Platzgründen und um eine höhere Übersichtlichkeit zu erzielen, werden die beiden Parameter von Ereignisprozeduren in diesem Buch meist nicht angegeben. Stattdessen wird das Listing in einer verkürzten Form entsprechend dem folgenden Muster dargestellt.
VORSICHT
HINWEIS
Private Sub Button1_Click(...) Handles Button1.Click ... End Sub
Für VB6-Kenner sieht die obige Deklaration redundant aus: Sowohl aus dem Prozedurnamen als auch aus dem Handles-Nachsatz scheint hervorzugehen, für welches Ereignis die Prozedur gedacht ist. Dabei ist aber Vorsicht angebracht: Anders als in VB6 ist in VB.NET der Prozedurname vollkommen gleichgültig. Sie können die Prozedur also von Button1_Click in Xyz umbennen – und sie funktioniert weiterhin unverändert. Entscheidend dafür, auf welches Ereignis die Prozedur reagiert, ist einzig das nach dem Schlüsselwort Handles angegebene Ereignis!
Wenn Sie ein Steuerelement aus einem Formular entfernen, bleiben die zum Steuerelement gehörenden Ereignisprozeduren erhalten. Allerdings wird der Nachsatz Handles steuerelementname.ereignisname entfernt, weil dieser Nachsatz syntaktisch falsch ist, solange es das Steuerelement gar nicht gibt. Wenn Sie nun das Steuerelement wieder einfügen (mit dem ursprünglichen Namen), wird der Handles-Nachsatz nicht mehr wiederhergestellt! Das bedeutet, dass die noch vorhandenen Ereignisprozeduren nicht mehr mit dem Steuerelement verbunden sind und daher wirkungslos bleiben. Abhilfe: Der Handles-Nachsatz muss manuell wieder hinzugefügt werden.
Datei laden und speichern Wenn es einmal gelungen ist, das Codegerüst für die Ereignisprozeduren zu füllen, muss nur noch der eigentliche Code eingefügt werden. Für das vorliegende Beispiel soll in den beiden Button-Ereignisprozeduren eine Datei geladen bzw. gespeichert werden. Zur Dateiauswahl werden vorgefertige Standarddialoge verwendet, die über die Steuerelemente Open- bzw. SaveFileDialog zur Verfügung steht. Die Anwendung dieser Dialoge sowie die Programmiertechniken zum Lesen bzw. Schreiben wurde bereits in Kapitel 10 beschrieben.
13.1 Einführung
567
Vom Standpunkt der Windows.Forms-Programmierung ist der Aufruf der Dialoge durch ShowDialog sowie die Auswertung des Rückgabewerts (Vergleich mit DialogResult.OK) interessant. Nach dem Laden der Datei wird einfach die Text-Eigenschaft des TextBox-Steuerelements geändert. Dadurch wird der gelesene Text im Textfeld angezeigt. Analog wird beim Speichern einfach der Inhalt des Textfelds ebenfalls aus der Text-Eigenschaft gelesen. Bemerkenswert ist noch die Variable textHasChanged, die auf Klassenebene deklariert wird. Diese Variable wird dazu verwendet, um aufzuzeichnen, ob sich der Text seit dem Laden bzw. Speichern der Datei geändert hat. Die Variable wird beim Programmende ausgewertet (siehe nächste Überschrift). Außer dem hier abgedruckten Code enthält die Formulardatei noch eine ganze Menge Zeilen, die automatisch von der Entwicklungsumgebung eingefügt wurden (Vom Windows Form Designer generierter Code). Dieser Code erfüllt zwei Aufgaben: •
Erstens speichert er alle Einstellungen, die während des Formularentwurfs vorgenommen wurden (Namen der eingefügten Steuerelemente, deren Position und Größe, ihre Eigenschaften etc.). Dieser Code wird bei jedem Wechsel zwischen der Code- und der Design-Ansicht ausgewertet bzw. aktualisiert.
•
Zweitens enthält der Code alle Anweisungen, damit die Steuerelemente beim Start des Programms sichtbar werden und beim Schließen des Formulars wieder aus dem Speicher freigegeben werden.
Nähere Informationen zu dem automatisch erzeugten Code finden Sie in Abschnitt 15.2.2. ' Beispiel windows.forms\intro Public Class Form1 Inherits System.Windows.Forms.Form [... Vom Windows Form Designer generierter Code ...] Dim textHasChanged As Boolean ' ANSI-Datei laden Private Sub Button1_Click(...) Handles Button1.Click If OpenFileDialog1.ShowDialog() = DialogResult.OK Then ' Datei lesen und in Textbox darstellen Dim enc As System.Text.Encoding = _ System.Text.Encoding.GetEncoding(1252) Dim sr As New IO.StreamReader(OpenFileDialog1.FileName, enc) TextBox1.Text = sr.ReadToEnd() sr.Close() textHasChanged = False End If End Sub
568
13 Windows.Forms – Einführung
' ANSI-Datei speichern unter ... Private Sub Button2_Click(...) Handles Button2.Click SaveFileDialog1.FileName = OpenFileDialog1.FileName If SaveFileDialog1.ShowDialog() = DialogResult.OK Then ' Datei lesen und in Textbox darstellen Dim enc As System.Text.Encoding = _ System.Text.Encoding.GetEncoding(1252) Dim sw As New IO.StreamWriter(SaveFileDialog1.FileName, _ False, enc) sw.Write(TextBox1.Text) sw.Close() textHasChanged = False End If End Sub
Sicherheitsabfrage beim Programmende Mit den beiden obigen Ereignisprozeduren ist das Programm bereits funktionsfähig. Die zwei folgenden Ereignisprozeduren machen das Programm aber noch ein wenig benutzerfreundlicher. Die Ereignisprozedur TextBox1_TextChanged wird immer dann aufgerufen, wenn sich im Textfeld etwas ändert. Darin wird die Variable textHasChanged auf True gesetzt. Somit kann das Programm jederzeit feststellen, ob der aus einer Datei geladene Text geändert wurde. Die Ereignisprozedur Form1_Closing wird automatisch ausgeführt, bevor das Fenster geschlossen werden soll (insbesondere, nachdem ein Benutzer den X-Button zum Schließen des Fensters angeklickt hat). Falls der Text zu diesem Zeitpunkt nicht gespeicherte Änderungen enthält, wird mit MsgBox ein kleiner Dialog angezeigt: Soll der Text vor dem Programmende gespeichert werden? Je nachdem, für welche der drei Antworten JA, NEIN oder ABBRUCH sich der Anwender entscheidet, wird das Programmende durch e.Cancel = True widerrufen oder der Text noch gespeichert. Dazu wird einfach die Button2_Click-Prozedur aufgerufen, wobei als Parameter zweimal Nothing übergeben wird. Private Sub TextBox1_TextChanged(...) Handles TextBox1.TextChanged textHasChanged = True End Sub Private Sub Form1_Closing(...) Handles MyBase.Closing ' Sicherheitsabfrage Dim result As MsgBoxResult If textHasChanged Then result = MsgBox("Soll der Text vor dem Programmende " + _ "gespeichert werden?", MsgBoxStyle.YesNoCancel)
13.2 Elementare Programmiertechniken
569
If result = MsgBoxResult.Cancel Then ' doch kein Programmende e.Cancel = True ElseIf result = MsgBoxResult.Yes Then Button2_Click(Nothing, Nothing) ' falls das scheitert: ebenfalls kein Programmende If textHasChanged Then e.Cancel = True End If End If End Sub End Class
13.2
Elementare Programmiertechniken
Fenster/Formulare/Dialoge anzeigen Wenn Sie ein Programm entwickeln, das aus einem einzigen Formular bzw. Fenster besteht, ist es keine große Kunst, das Fenster anzuzeigen: Es erscheint beim Programmstart automatisch und verschwindet zum Programmende ebenso automatisch. (Genau genommen ist es umgekehrt: Wenn das Fenster geschlossen wird, endet automatisch auch das laufende Programm.) Was bei einem einzigen Fenster ganz einfach ist, wird bei mehreren Fenstern komplizierter: •
Wann soll das Programm enden? Wenn das Hauptfenster geschlossen wird? Oder wenn das letzte offene Fenster geschlossen wird?
•
Soll das Hauptfenster bedienbar bleiben, während ein Dialogfenster sichtbar ist?
Bei Programmen, die aus einem Hauptfenster und mehreren Dialogen bestehen, sieht die gängigste Vorgehensweise folgendermaßen aus: In einer Ereignisprozedur des Hauptfensters – z.B. als Reaktion auf einen Mausklick oder eine Menüauswahl – wird der Dialog auf die folgende Weise dargestellt. ShowDialog bewirkt, dass das Hauptprogramm blockiert ist, bis der Dialog beendet wurde. (Der Dialog wird in diesem Fall als modal bezeichnet.) Falls der Dialog einen Rückgabewert liefert, liefert ShowDialog diesen zurück. Private Sub Button1_Click(...) Handles Button1.Click Dim frm As New FormXyz() frm.ShowDialog() oder result = frm.ShowDialog() ... Eingabe auswerten frm.Dispose() End Sub FormXyz ist in diesem Fall der Name des Dialogformulars (per Default also Form1, Form2 etc.). Intern gilt jedes Formular als eine eigene, selbst definierte Klasse. Durch New erzeugen Sie ein neues Objekt dieser Klasse, und durch ShowDialog machen Sie das Objekt
570
13 Windows.Forms – Einführung
VERWEIS
(das Formular) sichtbar. Dispose entfernt das Objekt anschließend wieder aus dem Speicher. Statt ShowDialog können Sie auch die Methode Show verwenden. Das zweite Fenster ist dann nicht modal, sondern kann (fast) gleichberechtigt zum ersten Fenster verwendet werden. Die Methode Show sowie eine Menge anderer Mechanismen zur Anzeige, Verwaltung und zum Datenaustausch zwischen mehreren Fenstern werden in Abschnitt 15.3 detailliert vorgestellt.
Fenster bzw. Programm beenden Wenn ein VB.NET-Programm aus nur einem Fenster besteht, wird das Programm automatisch beendet, wenn das Fenster vom Anwender geschlossen wird. Um das Fenster per Programmcode zu schließen (z.B. als Reaktion auf das Anklicken des ENDE-Buttons), führen Sie Me.Close aus. In der Folge werden die Ereignisse Closing und Closed ausgelöst (siehe unten). Anschließend wird das Fenster mit all seinen Steuerelementen aus dem Speicher entfernt (d.h., es wird automatisch Dispose ausgeführt). Um ein Fenster nur vorübergehend unsichtbar zu machen, führen Sie Me.Hide aus. Das Form-Objekt kann dann später mit frmobject.Show wieder angezeigt werden. Dieses Szenario ist nur bei Programmen mit mehreren Fenstern oder Modulen sinnvoll und wird ebenfalls in Abschnitt 15.3 behandelt.
Standarddialoge Manchmal ist es gar nicht notwendig, einen eigenen Dialog zu erstellen. Für immer wieder vorkommende Aufgaben – etwa zur Auswahl einer Datei oder einer Schriftart – gibt es so genannte Standarddialoge. Der wohl am häufigsten eingesetzte Standarddialog ist die Nachrichtenbox, die mit MsgBox (nur VB.NET) oder mit MsgBox.Show (alle .NET-Programmiersprachen) dargestellt werden kann. Standarddialoge werden in Abschnitt 15.5 ausführlicher vorgestellt.
Grafik in Formularen und Steuerelementen In Formularen können nicht nur Steuerelemente dargestellt werden, sondern auch Grafikausgaben durchgeführt werden. Dazu verwenden Sie eine Paint-Ereignisprozedur, die automatisch immer dann aufgerufen wird, wenn ein Teil des Fensters neu gezeichnet werden muss. (Sie müssen Ihre Grafikausgaben also immer wieder wiederholen, nachdem Teile des Fensters verdeckt waren. Das Programm merkt sich Ihre Grafikkommandos nicht.) Die folgenden Zeilen zeigen, wie Sie eine rote Linie vom linken oberen Eck zum Punkt (100,100) zeichnen. Die Einheit des Koordinatensystems ist per Default Pixel. Bemerkenswert an der kurzen Prozedur ist der Umstand, dass hier zum ersten Mal ein Parameter einer Ereignisprozedur verwendet wird: Der Parameter e ist bei Paint-Ereignissen ein Ob-
13.2 Elementare Programmiertechniken
571
jekt der Klasse PaintEventArgs. Dessen Eigenschaft Graphics verweist auf ein Objekt der Graphics-Klasse, mit dessen Hilfe sämtliche Grafikausgaben durchgeführt werden. Private Sub Form1_Paint(ByVal sender As Object, _ ByVal e As System.Windows.Forms.PaintEventArgs) Handles MyBase.Paint e.Graphics.DrawLine(Pens.Red, 0, 0, 100, 100) End Sub
Grafikausgaben können nicht nur direkt im Fenster, sondern auch in vielen Steuerelementen durchgeführt werden. Die vielen Details der Grafikprogrammierung werden im umfangreichen Kapitel 16 beschrieben.
Initialisierungsarbeiten (Load-Ereignis) Oftmals möchten Sie vor dem Erscheinen eines Fensters Variablen oder Steuerelemente initialisieren. Der geeignet Ort ist die Load-Ereignisprozedur des Formulars. Diese Prozedur wird unmittelbar vor dem Erscheinen des Formulars am Bildschirm ausgeführt. (Load ist das Defaultereignis von Formularen. Daher können Sie das Codegerüst für die Prozedur einfach durch einen Doppelklick auf das Formular im Design-Modus einfügen.)
Aufräumarbeiten (Closed-Ereignis)
HINWEIS
Falls Sie nach dem Schließen des Fensters Aufräumarbeiten durchführen möchten, ist die Closed-Ereignisprozedur der richtige Ort. (Die im Fenster enthaltenen Steuerelemente werden übrigens automatisch durch Dispose freigegeben. Darum kümmert sich der von der Entwicklungsumgebung eingefügte Code – siehe auch Abschnitt 15.2). Vor dem Close-Ereignis tritt das im Beispielprogramm bereits angewendete ClosingEreignis ein. Es drückt die Absicht des Anwenders aus, das Fenster zu schließen. Zu diesem Zeitpunkt kann das noch verhindert werden (e.Cancel=True).
Programmzugriff auf Fenstereigenschaften, Steuerelemente etc. Innerhalb des Codes zu einem Formular können Sie auf die Eigenschaften und Methoden des Formulars unmittelbar zugreifen. Darüber hinaus können Sie auf alle im Formular enthaltenen Steuerelemente direkt zugreifen. Left=10 verändert daher die Eigenschaft Left des Formulars. Button1.Left=10 verändert die Eigenschaft Left des Steuerelements Button1. Wenn Sie möchten, können Sie beim Zugriff auf Eigenschaften, Methoden und Steuerelemente das Schlüsselwort Me voranstellen (also Me.Left=10 bzw. Me.Button1.Left=10). Die Bedeutung des Codes ändert sich dadurch nicht, manche Programmierer empfinden den Code dann aber als klarer und besser lesbar. In den Beispielen dieses Buchs wird Me meistens verwendet.
572
13 Windows.Forms – Einführung
Spezialeffekte für Fenster Dieser Abschnitt beschreibt ganz kurz, wie Sie einige manchmal benötigte Spezialeffekte erzielen können. Mehr Details zu den hier erwähnten Eigenschaften gibt Abschnitt 15.1. •
Startposition des Fensters: Per Default entscheidet das Betriebssystem darüber, wo das Fenster erscheint. Wenn Sie eine andere Position erzielen möchten, verändern Sie die Eigenschaft StartPosition. Bei der Einstellung Manual werden die Koordinatenangaben der Eigenschaft Location berücksichtigt. (Beachten Sie, dass die Taskleiste bei manchen Rechnern am linken Bildschirmrand platziert ist. Daher ist eine x-Koordinate von 0 ungünstig!) Sie können die Startposition auch in Form_Load durch die Methode SetDesktopLocation einstellen.
•
Startgröße des Fensters: Per Default ist das Fenster beim Programmstart so groß wie im Design-Modus in der Entwicklungsumgebung. Der einfachste Weg, die Fenstergröße zu ändern, besteht demnach darin, die Größe im Design-Modus zu ändern. Wenn es aber aus irgendeinem Grund erforderlich ist, dass das Fenster im Design-Modus und im laufenden Programm unterschiedlich groß sein soll, können Sie die gewünschte Größe in Form_Load mit der Methode SetDesktopBounds angeben. (Gleichzeitig müssen Sie auch die Position angeben, die Sie aus der Eigenschaft mit DesktopLocation lesen können. Die folgende Zeile ändert die Außengröße des Fensters auf 500*100 Punkte, belässt die Position aber unverändert. Me.SetDesktopBounds(Me.DesktopLocation.X, Me.DesktopLocation.Y, _ 500, 100)
Falls Sie möchten, dass das Fenster beim Programmstart in maximaler Größe angezeigt wird, brauchen Sie sich nicht mit SetDesktopBounds zu plagen. Stattdessen stellen Sie einfach WindowState auf Maximized. •
Unveränderliche Fenstergröße: Wenn Sie möchten, dass die Fenstergröße nicht geändert werden darf, stellen Sie die Eigenschaft FormBorderStyle auf FixedSingle. (Auch die Einstellungen FixedDialog, Fixed3D und FixedToolWindow erzielen den gewünschten Effekt, verändern aber außerdem das Aussehen des Fensters.)
•
Fenster ohne Rahmen (Splash-Fenster): Wenn das Fenster ohne Rahmen (also insbesondere ohne Titelleiste) angezeigt werden soll, stellen Sie FormBorderStyle auf None. Beachten Sie, dass das Fenster nun weder verschoben noch geschlossen werden kann! Diese Einstellung ist im Regelfall nur für so genannte Splash-Fenster sinnvoll, die während des Programmstarts angezeigt werden.
•
Fenster immer ganz oben halten: Damit das Fenster immer über allen anderen Fenstern angezeigt wird, verwenden Sie die Einstellung TopMost=True.
•
Durchscheinende Fenster: Wenn Ihr Fenster durchscheinend aussehen soll, stellen Sie die Eigenschaft Opacity auf einen Wert kleiner 1, beispielsweise auf 0,5. (0 würde das Fenster vollkommen unsichtbar machen – das ist natürlich nicht sinnvoll.) Beachten Sie, dass dieser Effekt nur von Windows 2000/XP unterstützt wird.
13.3 Windows.Forms-Klassenhierarchie
573
•
Halb-durchsichtige Fenster: Einen ähnlichen Effekt können Sie erzielen, wenn Sie mit der Eigenschaft TransparencyKey eine im Fenster vorkommende Farbe als durchsichtig deklarieren: Dann erscheint das Fenster an allen Stellen, an denen diese Farbe vorkommt, vollkommen durchsichtig. (Vorsicht: An diesen Stelle kann die Maus nicht verwendet werden!) Für den oben vorgestellten Texteditor können Sie z.B. die Farbe Weiß einstellen: Dann können Sie durch den Text durchsehen! Auch dieser Effekt wird nur unter Windows 2000/XP unterstützt.
•
Nicht-rechteckige Fenster: Dieses Buch geht davon aus, dass Ihre Fenster rechteckig sind. Wenn Sie bereit sind, einigen Mehraufwand zu investieren, können Sie aber auch Fenster in einer beliebigen anderen Form erzeugen. Eine Anleitung im Internet finden Sie, wenn Sie auf den Microsoft-Developer-Seiten nach Shaped Windows Forms and Controls suchen: http://msdn.microsoft.com/library/en-us/dv_vstechart/html/ vbtchShapedWindowsFormsControlsInVisualStudioNET.asp
VERWEIS
Interna der Formularverwaltung Um die Funktionsweise von Formularen, aber auch des Formular-Designers richtig verstehen zu können, sind noch einige Detailinformationen darüber erforderlich, wie Eigenschaftseinstellungen im Code gespeichert werden, wie Steuerelemente in das Form-Objekt eingefügt werden, wo die Codeausführung beginnt und wo sie endet etc. Diese Hintergründe werden in Abschnitt 15.2 ausführlich beschrieben. Dort finden Sie unter anderem auch eine Erklärung, welche Funktion der von der Entwicklungsumgebung eingefügte Code hat (Vom Windows Form Designer generierter Code).
13.3
Windows.Forms-Klassenhierarchie
Formulare und viele Steuerelemente besitzen jeweils gemeinsame Ereignisse, Methoden und Eigenschaften. Warum das so ist, verstehen Sie sofort, wenn Sie einen Blick auf den folgenden Kasten mit der Hierarchie der wichtigsten Windows.Forms-Klassen werfen. (Beachten Sie insbesondere die Position der Klasse Form innerhalb der Hierarchie! Diese Klasse ist für die Darstellung von Fenstern verantwortlich. Beachten Sie auch, dass der Windows.Forms-Namensraum Hunderte von Klassen enthält und dieser Überblick daher alles andere als vollständig ist!)
574
13 Windows.Forms – Einführung
Klassenhierarchie im System.Windows.Forms-Namensraum (Teil 1) Object └─ MarshalByRefObject └─ Component
│ ├─ CommonDialog │ └─ Control └─ ...
.NET-Basisklasse Objekt als Referenz an andere Rechner weitergeben Basisklasse für Komponenten (im Namensraum System.ComponentModel) Basisklasse für Standarddialoge (z.B. ColorDialog, FileDialog, FontDialog etc.) Basisklasse für fast alle Steuerelemente
Klassenhierarchie im System.Windows.Forms-Namensraum (Teil 2) ...
└─ Control │ ├─ ButtonBase │ ├─ ScrollableControl │ │ │ └─ ContainerControl │ │ │ ├─ Form │ └─ UserControl │ └─ TextBoxBase
Basisklasse für alle Steuerelemente; direkt abgeleitet sind z.B. Label, PictureBox, ScrollBar, GroupBox Basisklasse für Button-Steuerelemente (z.B. Button, CheckBox, RadioButton) Basisklasse für Steuerelemente, die automatisches Scrolling des Inhalts erlauben (z.B. Panel) Basisklasse zur Verwaltung des Eingabefokus für die enthaltenen Steuerelemente Klasse für Formlare, Fenster und Dialoge; davon Basisklasse für eigene Steuerelemente; abgeleitet ist z.B. PrintPreviewDialog Basisklasse für Steuerelemente zur Texteingabe (z.B. TextBox, RichTextBox)
14 Steuerelemente Dieses Kapitel gibt zuerst einen Überblick über die mit VB.NET mitgelieferten Steuerelemente. Nach einer Beschreibung einiger gemeinsamer Merkmale werden die einzelnen Steuerelemente im Detail vorgestellt. Dabei habe ich besonderen Wert auf die Präsentation von Programmiertechniken gelegt, die bei der Durchführung häufig benötigter Operationen helfen (z.B. die Validierung von Texteingaben und die Verwaltung und Sortierung von Listenelementen) Das Kapitel endet mit einigen Spezialthemen. Hierbei geht es z.B. um die effiziente Verwaltung gleichartiger Steuerelemente (das, was in VB6 Steuerelementfelder waren), um das dynamische Einfügen neuer Steuerelemente per Code und um die Entwicklung eigener Steuerelemente. 14.1 14.2 14.3 14.4 14.5 14.6 14.7 14.8 14.9 14.10 14.11 14.12
Einführung Gemeinsame Eigenschaften, Methoden und Ereignisse Buttons Textfelder Grafikfelder Listenfelder Datums- und Zeiteingabe Schiebe- und Zustandsbalken, Drehfelder Gruppierung von Steuerelementen Spezielle Steuerelemente Programmiertechniken Neue Steuerelemente programmieren
576 581 596 600 610 612 662 665 669 674 690 696
576
14.1
14 Steuerelemente
Einführung
14.1.1 Steuerelemente – Überblick In VB6 gab es eine Unterscheidung zwischen Standard- und Zusatzsteuerelementen. Außerdem gab es die besonders ressourcenschonenden Windowless-Steuerelemente. In VB.NET gibt es diese Unterscheidung nicht mehr. Alle Steuerelemente basieren auf derselben Technologie.
Windows.Forms-Steuerelemente Die folgenden Steuerelemente sind Teil des Systems.Windows.Forms-Namensraums und stehen in der Registrierkarte WINDOWS FORMS der Toolbox zur Auswahl. Sie stellen also gewissermaßen die Standardsteuerelemente von .NET dar. Elementare Windows.Forms-Steuerelemente Buttons
Button, CheckBox, RadioButton
Textfelder
Label, LinkLabel, TextBox, RichTextBox
Grafik
PictureBox (in vielen anderen Steuerelementen können aber
ebenso eine Bitmap dargestellt bzw. Grafikmethoden ausgeführt werden) Bitmap-Container
ImageList
Listenfelder
ListBox, CheckedListBox, ComboBox, ListView
Hierarchische Listen
TreeView
Tabellenfeld
DataGrid
Eigenschaftsfeld
PropertyGrid (zeigt die Eigenschaften eines beliebigen Objekts an;
das Steuerelement entspricht dem Eigenschaftsfenster der Entwicklungsumgebung und wird in diesem Buch nicht beschrieben) Zeit, Datum
DateTimePicker, MonthCalender
Gruppierung
GroupBox, Panel, TabControl (Dialogblätter)
Bildlaufleisten
HScrollBar, VScrollBar, Trackbar
Drehfeld
DomainUpDown, NumericUpDown
Zustandsanzeige
Progressbar
Fensterteiler
Splitter
Zeitgeber
Timer
Infotext anzeigen
ToolTip
Fehlerindikator
ErrorProvider
Hilfefenster anzeigen
HelpProvider
Programmindikator
NotifyIcon
14.1 Einführung
577
Windows.Forms-Steuerelemente zur Gestaltung einer Benutzeroberfläche (Kapitel 15) Menüs
MainMenu, Menu, MenuItem, ContextMenu
Symbolleiste
ToolBar
Statusleiste
StatusBar
Standarddialoge
OpenFileDialog, SaveFileDialog, FontDialog, ColorDialog
Windows.Forms-Steuerelemente zum Ausdruck eines Dokuments (Kapitel 17) PrintDocument (Namensraum System.Drawing.Printing)
Drucker einstellen
PrintDialog
Seite einstellen
PageSetupDialog
Seitenvorschau
PrintPreviewDialog, PrintPreviewControl
Datenbankberichte
CrystalReportViewer
HINWEIS
Ausdruck steuern
Die mit VS.NET nur aus Kompatibilitätsgründen mitgelieferten ActiveX-Steuerelemente Microsoft Chart Control und Microsoft Masked Edit Control sind gegenüber VB6 unverändert. Ihre Anwendung in neuen Projekten wird nicht mehr empfohlen. Die Steuerelemente werden deswegen in diesem Buch nicht beschrieben. Eine Referenz der Eigenschaften, Methoden etc. finden Sie in der Hilfe, wenn Sie nach Referenz älteren ActiveX suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/vbcmn/html/vborilegacyactivexcntrlref.htm
Abbildung 14.1: Die Windows.Forms-Steuerelemente in der Toolbox
Datenbanksteuerelemente Viele der oben aufgezählten Steuerelemente können auch als so genannte gebundene Steuerelemente verwendet werden, um die Daten einer ADO.NET-Datenbankabfrage darzustellen. Da die Datenbankfunktionen bereits in den Standardsteuerelementen integriert
578
14 Steuerelemente
sind, gibt es in VB.NET keine eigenen Datenbanksteuerelemente mehr. Die Datenbankanwendung der Steuerelemente wird in diesem Buch allerdings aus Platzgründen nicht beschrieben. Stattdessen finden Sie entsprechende Informationen in einem zweiten VB.NET-Band speziell zum Thema Datenbanken und Internet (siehe http://www.kofler.cc).
14.1.2 Microsoft.VisualBasic.Compatibility.VB6Steuerelemente
VORSICHT
Der in der Überschrift genannte Namensraum (Bibliothek Microsoft.VisualBasic.Compatibility.dll) enthält eine Reihe von Steuerelementen, die in VB.NET nicht oder nicht mehr in dieser Form zur Verfügung stehen. Der Namensraum enthält unter anderm die Steuerelemente Drive-ListBox, DirListBox und FileListBox sowie eine Reihe von XxxArray-Steuerelementen, mit denen die nicht mehr unterstützten Steuerelementfelder für die wichtigsten Standardsteuerelemente nachgebildet werden können. Der Namensraum wird vom Migrationsassistenten verwendet, um ansonsten nicht portierbaren VB6-Code in VB.NET umzuwandeln. Grundsätzlich ist es aber natürlich möglich, die in Microsoft.VisualBasic.Compatibility.VB6 enthaltenen Steuerelemente auch in neuen VB.NET-Projekten zu verwenden. Die Dokumentation rät davon aber ausdrücklich ab, da der Namensraum von zukünftigen VB.NET-Versionen möglicherweise nicht mehr unterstützt wird.
14.1.3 ActiveX-Steuerelemente (alias COM-Steuerelemente) Herkömmliche Zusatzsteuerelemente (in der Nomenklatur von VB6), die auch als ActiveXoder als COM-Steuerelemente bezeichnet werden, können in .NET weiterhin verwendet werden.
TIPP
Sobald ein COM-Steuerelement in ein Formular eingefügt wird, erzeugt die Entwicklungsumgebung zwei DLL-Bibliotheken (so genannte interop-Bibliotheken) im bin-Verzeichnis des Projekts, die für die Kommunikation zwischen dem COM-Steuerelement und .NET verantwortlich sind. Das COM-Steuerelement wird dabei von der Windows.Forms-Klasse Control abgeleitet und kann wie ein normales .NET-Steuerelement verwendet werden: seine Eigenschaften können im Eigenschaftsfenster verändert werden, die Ereignisse stehen im Codefenster zur Auswahl etc. Bemerkenswert ist, dass die Steuerelemente sogar mit einigen .NET-Eigenschaften ausgestattet sind (z.B. Anchor und Dock). Um den ActiveX-spezifischen Eigenschaftsdialog des Steuerelements zu öffnen, klicken Sie im unteren Ende des Eigenschaftsfensters den Link ACTIVEX-EIGENSCHAFTEN an. Sie können auf diese Weise einige Eigenschaften des Steuerelements einstellen, die im gewöhnlichen Eigenschaftsfenster nicht verändert werden können.
HINWEIS
14.1 Einführung
579
Bei ActiveX-Steuerelementen gibt es Design- und Run-time-Lizenzen. Damit ein ActiveX-Steuerelement in der Entwicklungsumgebung verwendet werden kann, muss auf dem lokalen Rechner die Design-Lizenz installiert sein. Diese liegt vor, die VB6 am Rechner installiert wurde. Wenn das nicht der Fall ist, können Sie die Datei extras\vb6 controls\vb6controls.reg durch Doppelklick ausführen. Die Datei enthält die Design-Lizenzen für die VB6-Steuerelemente in Form von Einträgen für die Registrierdatenbank. (Sie finden die Datei vb6controls.reg auf der VS.NET-Installations-CD.)
Kompatibilität Ich habe die Verwendung von COM-Steuerelementen unter .NET zu wenig intensiv getestet, um seriöse Aussagen über die zu erwartenden Kompatibilitätsprobleme oder über Schwierigkeiten bei der Weitergabe derartiger Projekte machen zu können. Dass die Migration vollkommen problemlos verläuft, ist allerdings nicht anzunehmen: Beispielsweise habe ich die Verwendung des CheckBox-Steuerelements aus der MS-Forms2.0-Bibliothek ausprobiert. Dabei hat sich herausgestellt, dass ausgerechnet die absolut elementare Value-Eigenschaft fehlt. Die Suche im Objektbrowser hat mich dann auf die neue Methode get_Value gebracht. Diese Methode liefert allerdings ein Object als Ergebnis. Weitere Tests ergaben, dass mit CBool eine Umwandlung in True oder False möglich ist. Die folgende Ereignisprozedur verändert den Text des CheckBox-Steuerelements je nachdem, in welchem Zustand sich das Steuerelement gerade befindet. ' Beispiel steuerelemente\com-steuerelemente Private Sub AxCheckBox1_Change(...) Handles AxCheckBox1.Change If CBool(AxCheckBox1.get_Value()) = True Then AxCheckBox1.Caption = "CheckBox wurde ausgewählt" Else AxCheckBox1.Caption = "CheckBox ist nicht ausgewählt" End If End Sub
Einige wenige ActiveX-Steuerelementen sind definitiv inkompatibel zu .NET und können überhaupt nicht verwendet werden. Dazu zählen UpDown, ssTab und Coolbar. Für alle drei Steuerelemente gibt es aber .NET-Alternativen, so dass der Verlust verschmerzbar ist.
14.1.4 Tipps zur Verwendung der Toolbox In der Toolbox wird in mehreren so genannten Registrierkarten eine Defaultauswahl von Steuerelementen angezeigt. Für die Windows-Programmierung enthält die in Abbildung 14.1 dargestellte Registierkarte die wichtigsten Steuerelemente. Grundsätzlich gibt es zwei Darstellungsweisen innerhalb der Toolbox: Per Default werden die Steuerelemente als Liste dargestellt, d.h. jede Zeile enthält ein Icon und den Namen des Steuerelements. Wenn Sie sich an die Icons gewöhnt haben, können Sie das Fenster über
580
14 Steuerelemente
ein Kontextmenükommando wie in Abbildung 14.1 auf die platzsparende Iconansicht umstellen. Die Icons sind innerhalb der Toolbox geordnet. Die Ordnung sieht zwar auf ersten Blick willkürlich aus, aber im Regelfall sind zusammenpassende Steuerelemente in Gruppen angeordnet. Wenn Sie möchten, können Sie die Steuerelemente mit der Maus verschieben und so die Reihenfolge verändern. Per Kontextmenü können Sie die Steuerelemente auch alphabetisch sortieren.
Steuerelemente in die Toolbox einfügen Neben den in der Toolbox bereits enthaltenen Steuerelementen können Sie mit dem Kontextmenü TOOLBOX ANPASSEN weitere Elemente einfügen. Im Einfügedialog (siehe Abbildung 14.2) haben Sie die Wahl zwischen installierten COM- und .NET-Steuerelementen. Wenn Sie das gewünschte Steuerelement nicht finden (was z.B. bei selbst programmierten Steuerelementen die Regel ist), können Sie mit DURCHSUCHEN nach der DLL-Datei suchen, die das Steuerelement enthält.
Abbildung 14.2: Dialog zum Einfügen eines zusätzlichen Steuerelements in die Toolbox
Das Steuerelement wird in die gerade aktive Registrierkarte eingefügt. Sie können die Steuerelemente aber per Maus zwischen unterschiedlichen Registrierkarten verschieben bzw. überhaupt neue Registrierkarten einfügen, um Ordnung in die Steuerelementlisten zu bekommen. Die Veränderungen an der Toolbox gelten für die gesamte Entwicklungsumgebung (also nicht nur für das aktuelle Projekt).
HINWEIS
14.2 Gemeinsame Eigenschaften, Methoden und Ereignisse
581
Wenn Sie ein Steuerelement von der Toolbox in ein Formular einfügen, fügt die Entwicklungsumgebung automatisch auch einen Verweis auf die Bibliothek hinzu, die das Steuerelement enthält. (Sie können das im Objektkatalog oder im Projektmappen-Explorer kontrollieren.) Wenn Sie das Steuerelement wieder aus dem Formular löschen, wird der Bibliotheksverweis dagegen nicht automatisch entfernt.
14.2
Gemeinsame Eigenschaften, Methoden und Ereignisse
Dieser Abschnitt beschreibt gemeinsame Eigenschaften, Methoden und Ereignisse, die bei vielen Steuerelementen zur Verfügung stehen. (Aber natürlich gilt: Nicht jede der in diesem Abschnitt beschriebenen Eigenschaften steht für jedes Steuerelement zur Verfügung!) Viele der hier erwähnten Schlüsselwörter können auch für das Form-Objekt verwendet, das das zugrunde liegende Formular beschreibt. Um hier eine ebenso langweilige wie unübersichtliche Aufzählung zu vermeiden, wurden die Schlüsselwörter in thematischen Gruppen zusammengefasst. Eine alphabetische Referenz der erwähnten Schlüsselwörter finden Sie in der Syntaxzusammenfassung am Ende des Abschnitts.
14.2.1 Aussehen Das Aussehen von Steuerelementen wird sehr stark durch das Aussehen des zugrunde liegenden Formulars beeinflusst! Wenn Sie beispielsweise die Hintergrundfarbe des Formulars ändern, dann nehmen automatisch auch alle Steuerelemente diese Hintergrundfarbe an, sofern nicht explizit eine andere Farbe eingestellt wurde. Dasselbe Verhalten gilt auch für eine Reihe weiterer Eigenschaften (z.B. für die Schriftart).
Text Mit der Eigenschaft Text geben Sie die Beschriftung des Steuerelements an. Einige Steuerelemente stellen zu lange Texte per Default in mehreren Zeilen dar. Bei vielen Steuerelementen kann die Positionierung und Ausrichtung des Texts (z.B. links oben in einem Button) durch TextAlign eingestellt werden. Die Schriftart und -größe wird durch die Eigenschaft Font eingestellt. Dabei sind nur TrueType- und OpenType-Schriftarten zulässig. Beachten Sie auch, dass Font-Objekte nicht verändert werden können. Wenn Sie im laufenden Programm beispielsweise die Schriftart von normal auf fett umstellen möchten, müssen Sie ein neues Font-Objekt erzeugen. Das folgende Beispiel demonstriert die richtige Vorgehensweise. (Beachten Sie auch, dass das alte Font-Objekt durch Dispose freigegeben werden sollte.)
582
14 Steuerelemente
VERWEIS
Private Sub Button1_Click(...) Handles Button1.Click Dim fnt As New Font(Button1.Font, FontStyle.Bold) Button1.Font.Dispose() Button1.Font = fnt End Sub
Eine Menge weiterer Informationen zum Umgang mit Font-Objekten finden Sie in Abschnitt 16.3.
VORSICHT
.NET verwendet intern zur Darstellung aller Zeichenketten den Unicode-Zeichensatz. Das gilt natürlich auch für Steuerelemente. Ob die Steuerelemente allerdings tatsächlich Unicode-kompatibel sind, hängt auch von ihrer Darstellung durch das Betriebssystem ab! Wenn Ihr Programm unter Windows 98/ME ausgeführt wird (Windows 95 wird ja überhaupt nicht unterstützt), dann kann es laut Dokumentation bei den folgenden Steuerelementen Unicode-Darstellungsprobleme geben: TabControl, ListView, TreeView, DateTimePicker, MonthCalendar, TrackBar, ProgressBar, ImageList, ToolBar und StatusBar. Weitere Informationen finden Sie, wenn Sie in der Online-Hilfe nach Codierung Globalisierung Windows Forms suchen: ms-help://MS.VSCC/MS.MSDNVS.1031/vbcon/html/vbconUnicodeCharacterDisplayInWFCProjects.htm
Bilder, Grafik In den meisten Steuerelementen können auch Bilder dargestellt werden, wobei es mehrere Varianten gibt: •
Im Normalfall wird die Bilddatei durch das Anklicken der Image-Eigenschaft im Eigenschaftsfenster geladen. (Als Bilddatei sind nicht nur Bitmaps, sondern auch Icons und WMF/EMF-Dateien zulässig!) Bilder können ebenso wie Texte durch ImageAlign innerhalb des Steuerelements positioniert werden.
•
Wenn es im Formular einen ImageList-Container gibt (siehe Abschnitt 14.5.2), dann können Sie auf eine der dort enthaltenen Bitmaps über die beiden Eigenschaften ImageList und ImageIndex verweisen. Abermals dient ImageAlign zur Positionierung des Bilds.
•
Schließlich können Sie mit BackGroundImage ein Hintergrundmuster angeben. Im Gegensatz zu den beiden ersten Varianten wird das Bild nun periodisch wiederholt, um so den gesamten Hintergrund des Steuerelements mit einem Muster zu füllen.
Manche Steuerelementen stellen schließlich das Paint-Ereignis zur Verfügung. Damit können Sie im Steuerelement beliebige Grafikausgaben machen. Das eignet sich insbesondere zur Gestaltung von ansonsten leeren Steuerelementen (z.B. für einen Button ohne Text). Hintergründe zur Paint-Ereignisprozedur und zur Grafikprogrammierung werden in Ka-
14.2 Gemeinsame Eigenschaften, Methoden und Ereignisse
583
pitel 16 vermittelt. Ein Beispiel für eine Paint-Ereignisprozedur bei einem Button finden Sie in Abschnitt 14.3.1.
Farben Die Hintergrundfarbe von Steuerelementen kann mit BackColor, die Vordergrundfarbe (Textfarbe) mit ForeColor eingestellt werden. Beachten Sie aber, dass viele Anwender die Defaultfarben bevorzugen und dass zu viele Farben Ihr Programm unübersichtlich machen können.
HINWEIS
Wenn Sie die Farbe per Code verändern möchten, können Sie die Windows-Standardfarben der SystemColors-Aufzählung entnehmen (Namensraums System.Drawing). Darüber hinaus stehen Ihnen zahlreiche weiter Farben in der Form Color.White, Color.Black etc. zur Auswahl. Eigene Farben können Sie mit Color.FromArgb(rot, grün, blau) erzeugen. Details zum Umgang mit Farben finden Sie in Abschnitt 16.2.2. Eine Veränderung der Windows-Standardfarben können Sie durch das SystemColorsChanged-Ereignis feststellen. Das ist aber nur in wenigen Spezialanwendungen erforderlich. (Jedes Programm verwendet beim Start automatisch die gerade aktuellen Windows-Standardfarben. Eine dynamische Anpassung während der Programmausführung ist nur in seltenen Fällen notwendig.)
Umrandung Einige Steuerelemente können mit der Eigenschaft BorderStyle mit einem einfachen oder einem dreidimensionalen Rand ausgestattet werden.
Sichtbarkeit Ob ein Steuerelement sichtbar ist oder nicht, wird durch Visible gesteuert. Im Regelfall sind natürlich alle Steuerelemente sichtbar. In manchen Fällen kann es aber sinnvoll sein, einige Steuerelemente nur bei Bedarf anzuzeigen. Statt Visible direkt zu ändern, können Sie dazu auch die Methoden Hide und Show verwenden. Enable hat nur bedingt mit der Sichtbarkeit zu tun: Durch Enable=False deaktivieren Sie das Steuerelement. Es wird nun grau dargestellt, ist also gewissermaßen noch halb sichtbar. Es kann aber nicht mehr verwendet werden. Auch Enable ist per Default auf True gestellt. Es kann aber für die Anwender Ihres Programms hilfreich sein, wenn im Kontext gerade nicht einsetzbare Steuerelemente durch Enable=False deutlich markiert sind.
Manchmal besteht der Wunsch, Steuerelemente durchsichtig zu machen: Beispielsweise soll ein Beschriftungsfeld (Label) das Hintergrundmuster des Formulars nicht überdecken. Dazu stellen Sie einfach die Hintergrundfarbe BackColor auf Transparent. (Sie finden diese Farbe im Eigenschaftsfenster als erste Farbe der WEB-Gruppe.)
584
14 Steuerelemente
Änderungen in Steuerelementen (z.B. Label1.Text=...) werden im Regelfall erst am Ende der Ereignisprozedur sichtbar, in der die Änderung durchgeführt wird. Wenn in der Prozedur eine länger andauernde Berechnung ausgeführt wird, kann eine sofortige Aktualisierung durch Update erreicht werden. Noch umfassender ist die Wirkung von Refresh – die Methode bewirkt ein komplettes Neuzeichnen des Steuerelements (unabhängig davon, ob dies erforderlich ist oder nicht). Wenn nur Teile des Steuerelements neu gezeichnet werden sollen, können Sie dazu Invalidate verwenden.
Maus Das Aussehen der Maus wird durch die Eigenschaft Cursor bestimmt. Wenn diese Eigenschaft verändert wird, nimmt die Maus ein anderes Aussehen an, solange sie sich über dem Steuerelement befindet. Die Cursors-Klasse stellt einige Defaultformen für die Maus zur Auswahl.
3-D-Aussehen
VERWEIS
Steuerelemente werden üblicherweise mit einem leicht dreidimensionalen Aussehen dargestellt. Bei manchen Steuerelementen kann durch FlatStyle=Flat aber auch ein flaches, zweidimensionales Aussehen erreicht werden. Wenn Sie möchten, dass Steuerelemente unter Windows XP in der dort üblichen neuen Optik erscheinen, müssen Sie die Einstellung FlatStyle=System verwenden. Außerdem muss das Programm mit Version 6 der Bibliothek comctl32.dll verbunden werden, die zurzeit nur unter Windows XP zur Verfügung steht. Weitere Details zu diesem Vorgang folgen in Abschnitt 15.2.6.
VERWEIS
14.2.2 Größe, Position, Layout Positions- und Größenangaben erfolgen in der Regel durch Objekte der Strukturen Point, Size und Rectangle. Diese Strukturen werden in Abschnitt 16.2 im Rahmen der Grafikprogrammierung vorgestellt. Ihre Verwendung sollte aber auf Anhieb klar sein. Aus Eigenschaften wie X, Y, Width und Height können die Details der Position bzw. Größen entnommen werden.
Position: Die Position des linken oberen Ecks eines Steuerelements wird wahlweise durch Location (Point-Objekt) bzw. durch Left und Top bestimmt. Alle drei Eigenschaften sind veränderlich. Right und Bottom geben die Position des rechten unteren Ecks an. Diese beiden Eigen-
schaften können allerdings nur gelesen werden.
14.2 Gemeinsame Eigenschaften, Methoden und Ereignisse
585
Größe: Die Größe wird durch Size (Size-Objekt) oder durch Width und Height bestimmt. Position und Größe: Bounds verweist auf ein Rectangle-Objekt, das sowohl die Position als auch die Größe enthält. Innenmaße: Alle bisher angegebenen Eigenschaften bezogen sich auf die Außenmaße des Steuerelements. Bei manchen Steuerelementen gibt es davon abweichende Innenmaße, die um den Rand verkleinert sind und den Bereich des Steuerelements angeben, der tatsächlich genutzt werden kann (z.B. für Grafikausgaben oder als Container für andere Steuerelemente). Die Innenmaße können aus ClientSize (Size-Objekt) oder ClientRectangle (Rectangle-Objekt) entnommen werden. Bei ClientRectangle sind die Eigenschaften X und Y immer 0, weil das Innenkoordinatensystem innerhalb des Steuerelements bei (0,0) beginnt. Bei Steuerelementen, die nicht zwischen Innen- und Außenmaß unterscheiden, liefern Size und ClientSize einfach dieselben Ergebnisse. Größe und Position ändern: Zur Veränderung von Größe oder Position können Sie die meisten der oben angegebenen Eigenschaften ändern bzw. ein neues Objekt zuweisen. Beispielsweise versetzt die folgende Anweisung Button1 an die Position (10,10) und stellt gleichzeitig Breite und Höhe mit 100*50 Punkten ein. Button1.Bounds = New Rectangle(10, 10, 100, 50)
Die nächste Zeile setzt die Innengröße mit 100*50 Punkten fest. Button1.ClientSize = New Size(100, 50)
Alternativ können Sie die Außengröße auch mit SetBounds festlegen. Größe automatisch ändern: Manche Steuerelemente stellen die Eigenschaft AutoSize zur Verfügung. Wenn diese Eigenschaft auf True gesetzt wird, passt sich die Größe des Steuerelements automatisch an ihren Inhalt an. Das ist vor allem zur Darstellung von Bildern praktisch, kann aber in manchen Fällen auch für Text nützlich sein.
Steuerelemente verankern Mit der Anchor-Eigenschaft kann der Abstand zwischen dem Rand des Steuerelements und dem dazugehörenden Fensterrand fixiert werden. Das bedeutet, dass sich der Abstand bei einer Änderung der Fenstergröße nicht ändert. Je nachdem, an wie vielen Rändern das Steuerelement verankert ist, wird dazu die Position oder die Größe des Steuerelements verändert. Das Konzept ist anhand einiger Beispiele (und anhand von Abbildung 14.3) leicht zu verstehen: •
Per Default sind Steuerelemente links oben verankert (d.h. Anchor = AnchorStyles.Left Or AnchorStyles.Top oder salopper formuliert: Anchor=Left,Top). Bei einer Veränderung der Fenstergröße bleibt das Steuerelement unverändert.
•
Die Einstellung Anchor=Top,Right bewirkt, dass das Steuerelement bei einer horizontalen Vergrößerung des Fensters mit nach außen wandert.
586
14 Steuerelemente
•
Die Einstellung Anchor=Top,Left,Right bewirkt, dass das Steuerelement horizontal an beiden Seiten verankert ist. Bei einer Änderung der Fensterbreite ändert sich auch die Breite des Steuerelements.
•
Anchor=Top,Bottom,Left,Right bewirkt, dass das Steuerelement an allen Rändern veran-
kert ist. Es ändert sowohl seine Breite als auch seine Höhe synchron mit einer Änderung der Fenstergröße. •
Anchor=None bewirkt, dass das Steuerelement zentriert in der Mitte des Fensters angezeigt wird. Die Steuerelemente behalten dabei ihre ursprüngliche Größe. Wenn mehrere Steuerelemente so eingestellt sind, bleiben auch die relativen Abstände zwischen den Steuerelementen erhalten. (Dieses Verhalten für Anchor=None ist allerdings nicht dokumentiert.)
Abbildung 14.3: Die beiden Fenster demonstrieren die Funktion der Anchor-Eigenschaft
HINWEIS
Beachten Sie, dass die Größe einzelner Steuerelemente bei einer starken Verkleinerung des Fensters auf 0 schrumpfen kann bzw. dass sich einzelne Steuerelemente überlappen können (siehe Abbildung 14.4). Dabei kommt es zu keinen Fehlermeldungen, aber die Funktionstüchtigkeit des Programms ist beeinträchtigt. Dieses Problem können Sie umgehen, wenn Sie eine minimale Fenstergröße vorgeben (Eigenschaft MinimumSize des Fensters). Die Einstellung der Anchor-Eigenschaft gilt relativ zum Rand des Containers, in dem sich das Steuerelement befindet. Normalerweise dient das Formular als Container. Daneben gibt es aber auch eine Reihe von Steuerelementen (Panel, GroupBox, TabControl), die ebenfalls Steuerelemente aufnehmen können.
TIPP
14.2 Gemeinsame Eigenschaften, Methoden und Ereignisse
587
Sie können die Funktion der Anchor-Eigenschaft bereits im Entwurfsmodus ausprobieren: Ändern Sie einfach die Fenstergröße. Die Steuerelemente sollten ihre Größe automatisch anpassen.
Abbildung 14.4: Anchor-Probleme bei einer zu starken Verkleinerung des Fensters
Steuerelemente andocken Mit der Dock-Eigenschaft können Sie ein Steuerelement an einen Fensterrand andocken (Dock = Left, Right, Bottom oder Top). Das Steuerelement ist damit ganz am Rand festgeklebt. Außerdem nimmt das Steuerelement automatisch die gesamte Fensterbreite bzw. -höhe an (je nachdem, wo es angedockt wird). Mit Dock=Fill erreichen Sie, dass das Steuerelement den gesamten zur Verfügung stehenden Raum einnimmt, der nicht bereits von anderen angedockten Steuerelementen beansprucht wird. Beim Formular können Sie mit der Eigenschaft DockPadding angeben, wie groß der Abstand zwischen dem Rand und den angedockten Steuerelementen sein soll. Per Default ist der Abstand 0. Grundsätzlich ist es möglich, mehrere Steuerelemente in einem Fenster anzudocken. In der Praxis funktioniert das allerdings nur dann zufriedenstellend, wenn alle Steuerelemente nur horizontal oder nur vertikal angedockt werden. Wenn Sie mehrere Steuerelemente auf einer Seite nebeneinander andocken möchten, bestimmt die Einfügereihenfolge die Position (d.h., welches Steuerelement ganz am Rand, welches etwas weiter eingerückt ist etc.). Nachträglich können Sie die Reihenfolge gedockter Steuerelemente durch die Kontextmenükommandos IN DEN HINTERGRUND oder IN DEN VORDERGRUND verändern. Wenn Sie Steuerelemente sowohl horizontal als auch vertikal andocken möchten, besteht die beste Strategie darin, zuerst die Steuerelemente für eine Ausrichtung (vertikal oder horizontal) anzudocken. In den verbleibenden Leerraum fügen Sie ein Panel-Steuerelement ein und stellen dessen Dock-Eigenschaft auf Fill. Nun fügen Sie alle weiteren Steuerelemente in das Panel ein. Für diese Steuerelemente bestimmt die Dock-Eigenschaft die Platzierung innerhalb des Panels. Auf diese Weise können Sie beinahe beliebig komplexe Layouts er-
588
14 Steuerelemente
reichen. (Das Panel-Steuerelement ist ein Container für andere Steuerelemente. Im laufenden Programm ist es unsichtbar.) Das in Abbildung 14.5 dargestellte Beispielprogramm zeigt ein etwas komplexeres Layout (das aber durchaus realistisch ist und das Sie bei vielen E-Mail-Clients bzw. vergleichbaren Programmen wiederfinden werden). Die Steuerelemente wurden in der folgenden Reihenfolge in das Fenster eingefügt: •
ButtonBar-Steuerelement, Dock=Top
•
StatusBar-Steuerelement, Dock=Bottom
•
Panel-Steuerelement, Dock=Fill
•
TreeView-Steuerelement in Panel1, Dock=Left
•
ein weiteres Panel-Steuerelement in Panel1, Dock=Fill
•
ListView-Steuerelement in Panel2, Dock=Top
•
TextBox-Steuerelement in Panel2, Dock=Fill
VERWEIS
Abbildung 14.5: Die beiden Fenster demonstrieren die Funktion der Dock-Eigenschaft
Bei vielen Anwendungen mit gedockten Steuerelementen hat der Anwender die Möglichkeit, die Breite bzw. Höhe der gedockten Steuerelemente durch eine bewegliche Linie zu verändern. In .NET-Programmen können Sie diesen Effekt mit dem Splitter-Steuerelement erzielen, das in Abschnitt 14.10.1 beschrieben wird.
14.2 Gemeinsame Eigenschaften, Methoden und Ereignisse
589
14.2.3 Eingabefokus, Validierung Eingabefokus Ein Steuerelement besitzt den so genannten Eingabefokus, wenn es Tastatureingaben entgegennehmen kann. Pro Formular kann immer nur ein Steuerelement den Eingabefokus besitzen. Der Eingabefokus kann mit den Cursortasten, mit Alt-Tastenkürzeln (siehe unten), mit Tab sowie per Mausklick verändert werden. Bei einem Fokuswechsel tritt zuerst für das Steuerelement, das den Fokus bisher hatte, ein
HINWEIS
Leave-Ereignis auf, anschließend für das Steuerelement, das den Fokus nun erhält, ein Enter-Ereignis. Ob ein Steuerelement gerade den Fokus besitzt, können Sie mit der Eigenschaft ContainsFocus feststellen.
Neben den Enter- und Leave-Ereignissen gibt es auch die Ereignisse GotFocus und LostFocus. Die Dokumentation empfiehlt aber, diese Ereignisse nicht zu nutzen und stattdessen auf Enter und Leave zurückzugreifen.
Tabulaturreihenfolge (Aktivierreihenfolge) Wie gerade erwähnt, kann der Eingabefokus auch durch Tab verändert werden. (Wenn Sie möchten, dass ein Steuerelement nicht per Tab aktiviert werden kann, setzen Sie TabStop auf False.) Die Reihenfolge, mit der die Steuerelemente durch Tab angesprungen werden, wird durch die Eigenschaft TabIndex gesteuert. Das erste Steuerelement für die Tabulatorreihenfolge hat TabIndex=0, das zweite 1 etc. Zur Einstellung der Tabulatorreihenfolge verwenden Sie am besten das Menükommando ANSICHT|AKTIVIERREIHENFOLGE. Damit wird bei jedem Steuerelement die aktuelle TabIndex-Nummer angezeigt. Um die Reihenfolge zu ändern, klicken Sie die Steuerelemente einfach der Reihe nach an. Anschließend deaktiveren Sie ANSICHT|AKTIVIERREIHENFOLGE wieder.
Abbildung 14.6: Einstellung der Tabulatorreihenfolge
590
14 Steuerelemente
Alt-Tastaturabkürzungen Der Eingabefokus kann auch durch Alt-Tastenkürzel verändert werden. Derartige Tastenkürzel werden dadurch angezeigt, dass der betroffene Buchstabe in der Beschriftung des Steuerelements unterstrichen ist (also beispielsweise ABBRUCH, um den Button mit Alt+A auszuwählen). Um für ein Steuerelement ein Tastenkürzel zu definieren, geben Sie einfach in der Text-Eigenschaft vor dem betreffenden Buchstaben ein &-Zeichen an (also Text="&Abbruch"). Wenn Sie in einem Steuerelement das &-Zeichen anzeigen möchten, müssen Sie es im Text zweimal hintereinander angeben (z.B. Text="Drag && Drop") oder die Möglichkeit zur Definition von Tastenkürzeln durch UseMnemonic=False ganz deaktivieren. Bei Steuerelementen, für die kein Tastenkürzel definiert werden kann (z.B. TextBox), können Sie ein Label-Feld zur Beschriftung voranstellen und dieses mit einem Tastenkürzel ausstatten. Das Label-Feld kann den Eingabefokus selbst nicht erhalten und gibt ihn an das nächste Steuerelement in der Tabulatorreihenfolge weiter. Die Reaktion auf das Alt-Kürzel hängt vom Steuerelementtyp ab. Die meisten Steuerelemente erhalten einfach nur den Eingabefokus. Bei Buttons, Optionsfeldern und Auswahlkästchen wird das Steuerelement außerdem ausgewählt, d.h., das Alt-Kürzel hat diesselbe Wirkung wie ein Anklicken mit der Maus.
Fokus per Programmcode verändern Nicht nur die Anwender können den Fokus verändern (per Tastatur oder Maus), Sie können den Fokus auch per Programmcode auf ein anderes Steuerelement richten. Dazu führen Sie für das entsprechende Steuerelement einfach die Focus-Methode aus: Textbox1.Focus()
Die Methode liefert True oder False zurück, je nachdem, ob der Fokuswechsel geglückt ist oder nicht. (Der Fokuswechsel kann scheitern, wenn er von dem Steuerelement, das den Fokus momentan enthält, in der Validating-Ereignisprozedur verhindert wird.)
Validierung Es gibt verschiedene Zeitpunkte, zu denen Sie überprüfen können, ob Benutzereingaben im Steuerelement bzw. im gesamten Dialog korrekt sind. (Welche von diesen drei Varianten die beste ist, hängt von der jeweiligen Anwendung ab.) •
Bei jeder Änderung der Daten im Steuerelement: Dazu sehen viele Steuerelemente Changed-Ereignisse vor, z.B. TextChanged bei einem TextBox-Steuerelement.
•
Bei einem Fokuswechsel: Dazu ist das Ereignis Validating vorgesehen. Falls ein Eingabefehler festgestellt wird, kann durch e.Cancel=True der Fokuswechsel verhindert werden. Falls die Validating-Prozedur erfolgreich beendet wird (ohne e.Cancel=True), tritt danach ein Validated-Ereignis auf. Es kann dazu verwendet werden, eventuell im ValidatingCode dargestellte Fehlermeldungen, Farbveränderungen etc. rückgängig zu machen.
14.2 Gemeinsame Eigenschaften, Methoden und Ereignisse
•
591
Beim Beenden des Dialogs: In diesem Fall hängt der Ort für den Validierungscode von der Logik des Dialogs bzw. Fensters ab. Bei einfachen Dialogen eignet sich die Ereignisprozedur der OK-Buttons.
VERWEIS
Die Validating- und Validated-Ereignisse können durch die Einstellung CausesValidation = False unterbunden werden. Ein einfaches Beispiel für die Anwendung des Validating-Ereignisses zur Überprüfung, ob eine Zahleneingabe in einer TextBox korrekt durchgeführt wurde, finden Sie in Abschnitt 14.4.3. Die Anwendung des ErrorProvider-Steuerelements als Fehlerindikator wird in Abschnitt 14.10.5 demonstriert.
Reihenfolge der Ereignisse Wenn Sie mit Tab oder auf eine andere Weise den Fokus von steuerelement1 in steuerelement2 setzen, dann treten für die beiden Steuerelemente folgende Ereignisse in der hier angegebenen Reihenfolge auf: •
steuerelement1_Leave
•
steuerelement1_Validating
•
steuerelement1_Validated
•
steuerelement2_Enter
Der Fluss der Ereignisse kann in der Validating-Ereignisprozedur durch e.Cancel=True gestoppt werden. In diesem Fall bleibt der Fokus in steuerelement1.
14.2.4 Sonstiges Tastatur und Maus: Fast alle Steuerelemente können Tastatur- und Mauseingaben verarbeiten. Diese Eingaben lösen eine ganze Menge von Ereignissen aus (KeyDown, -Press, -Up sowie MouseEnter, -Leave, -Down, -Up, -Move und -Hover), die in den Abschnitten 15.9 und 15.10 beschrieben werden. Das Aussehen der Maus kann mit Cursor eingestellt werden. MousePosition und -Buttons geben Auskunft über die aktuelle Mausposition und den Zustand der Maustasten, ModifierKeys enthält außerden den Zustand von Alt, Shift etc. Die Methoden PointToScreen und PointToClient führen eine Umrechnung zwischen lokalen Koordinaten und absoluten Bildschirmkoordinaten durch. Jedes einzelne Steuerelement kann mit der Eigenschaft ContextMenu mit einem eigenen Menü ausgestattet werden, das erscheint, wenn die rechte Maustaste gedrückt wird. Datenbankanwendung (gebundene Steuerelemente): Die meisten Steuerelemente können mit so genannten Datenquellen verbunden werden. Derartige Datenquellen (z.B. DataTable- oder DataView-Objekte) stellen im Regelfall eine Verbindung zu einer Datenbank her.
592
14 Steuerelemente
Wenn die Eigenschaften DataSource und DataBinding des Steuerelements so eingestellt werden, dass sie auf die Datenquelle verweisen, werden im Steuerelement automatisch die Daten aus der Datenbank angezeigt (und können dort sogar verändert werden). Eigenschaften der Entwicklungsumgebung: Im Eigenschaftsfenster werden auch die Eigenschaften Locked und Modifiers angegeben. Dabei handelt es sich aber nicht um Eigenschaften des Steuerelements, sondern um Zusatzinformationen, die nur für die Verwaltung der Steuerelemente durch die Entwicklungsumgebung relevant sind. Locked=True bewirkt, dass das Steuerelement in der Entwicklungsumgebung vor Veränderungen geschützt ist. Modifiers gibt an, wie das Steuerelement innerhalb der Formularklasse deklariert werden soll. Per Default werden die Steuerelemente als Friend deklariert
und können daher per Code von anderen Formularen angesprochen werden (siehe auch Abschnitt 15.2.2). Verwaltung von Steuerelementen: Jedes Steuerelement befindet sich entweder direkt im Formular oder in einem anderen Steuerelement, das als Container funktioniert. Parent verweist auf das direkt übergeordnete Steuerelement, TopLevelControl auf den Container an der Spitze der Hierarchie (überlicherweise ein Form-Objekt). Bei Containern verweist Controls auf die enthaltenen Steuerelemente. GetChildAtPoint ermittelt, welches Steuerelement sich an einer bestimmten Position befindet. Bei jedem Steuerelement können Sie mit Name dessen Namen festste