Mehrstimmig

Wer mit Microsofts neuem Framework professionelle Softwareentwicklung betreiben will, wird Features wir Multithreading-Unterstützung und Ereignisbehandlung zu schätzen wissen. Wie man sie in .NET nutzt, zeigt der zweite Teil des Tutorials.

vorlesen Druckansicht 14 Kommentare lesen
Lesezeit: 16 Min.
Von
  • Michael Stal
Inhaltsverzeichnis

Ein Überblick über die .NET-Infrastruktur und elementare Konzepte von C# standen im Vordergrund des ersten Tutorial-Teils. Der zweite widmet sich den fortgeschritteneren Konzepten wie Meldungen über Events und Delegates, Multithreading, Zugriff auf und Bereitstellung von Metainformation, Umgang mit Assemblies, Cross-Language-Interoperabilität sowie dem Thema ‘Unmanaged Data und Unmanaged Code’.

Wer komplexe Anwendungen erstellt, stößt mindestens einmal auf folgendes Problem: Eine Komponente X möchte von einer Komponente Y Rückmeldung erhalten, wenn dort eine Änderung geschieht. Das einfachste und intuitivste Beispiel hierfür sind Windows-Programme, bei der ein benutzerdefinierter Event-Handler auf Ereignisse wie das Drücken von Buttons reagiert. Für gewöhnlich lässt sich zu diesem Zweck das Observer-Pattern [1] instanziieren, was aber mitunter zu einer Inflation von Klassen führt. Deshalb enthält .NET genau zu diesem Zweck eigene Programmkonstrukte: Delegates und Events. Ein Beispiel demonstriert das.

Listing 1a implementiert dazu einen Aktienticker (StockTicker, Listing 1a, Zeile 47) und Listing 1b steuert dessen Clientanwendung bei. Der Ticker verwaltet Aktieninformationen des Typs Share (1a, Zeile 9) in einer Hash-Tabelle (1a, Zeile 48) und verwendet als Primärschlüssel die Kurzbezeichnung der Aktie als Zeichenkette, also etwa ‘MSFT’ für Microsoft. Der Client (Testdriver, 1b, Zeile 6) möchte Rückmeldung erhalten, sobald sich Aktienwerte ändern. Rückmeldung bedeutet: Der Aktienticker soll in diesem Fall automatisch eine benutzerdefinierte Methode des Clients aufrufen, konkret die Methode OnChange (1b, Zeile 21). In dieser Methode soll der Aktienticker als Argumente erstens sich selbst als Ereignisquelle und zweitens eine spezielle Datenstruktur (ChangeEventArgs) zur genauen Beschreibung des Ereignisses übergeben.

Mehr Infos

Listing 1a und 1b

Listing 1b verwaltet als Client Aktieninformationen des Tickers, den Listing 1a implementiert.

Listing 1a

1  using System;
2 using System.Collections;
3
4 namespace Shares
5 {
6 public class NotFoundException : Exception {}
7 public class ExistsException : Exception {}
8
9 public struct Share {
10 private readonly string m_ShortName;
11 private readonly string m_CompanyName;
12 private readonly string m_URL;
13 private decimal m_Value;
14 public Share(string sn, string cn, string url) {
15 m_ShortName = sn;
16 m_CompanyName = cn;
17 m_URL = url;
18 m_Value = 0;
19 }
20 public Share(string sn, string cn, string url, decimal val)
21 : this(sn,cn,url) { m_Value = val; }
22 public string ShortName { get { return m_ShortName; } }
23 public string CompanyName { get { return m_CompanyName; } }
24 public string URL { get { return m_URL; } }
25 public decimal Value {
26 get { return m_Value; }
27 set { m_Value = value; }
28 }
29 }
30
31 sealed public class ChangeEventArgs : EventArgs {
32 private readonly string m_ShortName;
33 private readonly decimal m_OldValue;
34 private readonly decimal m_NewValue;
35 public ChangeEventArgs(string sn, decimal oldval, decimal newval) {
36 m_ShortName = sn;
37 m_OldValue = oldval;
38 m_NewValue = newval;
39 }
40 public string ShortName { get { return m_ShortName; } }
41 public decimal OldValue { get { return m_OldValue; } }
42 public decimal NewValue { get { return m_NewValue; } }
43 }
44
45 public delegate void ChangeEvent(object src, ChangeEventArgs e);
46
47 public class StockTicker {
48 protected Hashtable m_Map;
49 public event ChangeEvent OnChangeEvent;
50 public StockTicker() {
51 m_Map = new Hashtable(1013);
52 }
53 public Share this[string shortName] {
54 set {
55 if (m_Map.ContainsKey(shortName))
56 throw new ExistsException();
57 else
58 m_Map[shortName] = value;
59 }
60 get {
61 if (!m_Map.ContainsKey(shortName))
62 throw new NotFoundException();
63 else
64 return (Share)m_Map[shortName];
65 }
66 }
67 public void ChangeValue (string shortName, decimal newval) {
68 if (!m_Map.ContainsKey(shortName))
69 throw new NotFoundException();
70 else {
71 Share s = (Share)m_Map[shortName];
72 decimal oldval = s.Value;
73 s.Value = newval;
74 ChangeEventArgs args
75 = new ChangeEventArgs(shortName, oldval, newval);
76 if (OnChangeEvent != null)
77 OnChangeEvent(this, args);
78 }
79
80 }
81 }
82 }

Listing 1b

1  using System;
2 using Shares;
3
4 namespace StockTickerClient {
5
6 class TestDriver {
7 private StockTicker m_StockTicker;
8 public TestDriver()
9 {
10 m_StockTicker = new StockTicker();
11 Share share =
12 new Share("MSFT", "Microsoft", "www.microsoft.com", 63);
13 m_StockTicker["MSFT"] = share;
14 }
15 public void Run()
16 {
17 m_StockTicker.OnChangeEvent += new ChangeEvent(OnChange);
18 Share share = m_StockTicker["MSFT"];
19 m_StockTicker.ChangeValue("MSFT", 65);
20 }
21 public void OnChange(object src, ChangeEventArgs e)
22 {
23 Console.WriteLine("Value of {0} changed from {1} to {2}",
24 e.ShortName, e.OldValue, e.NewValue);
25 Console.ReadLine();
26 }
27 }
28 class Client
29 {
30 static void Main(string[] args)
31 {
32 new TestDriver().Run();
33 }
34 }
35 }

Um typisierte Funktionszeiger von einem Ereignis-Konsumenten an einen -Produzenten zu übermitteln, stellt C# das Sprachmittel der Delegates zur Verfügung. Im Beispiel:

public delegate void ChangeEvent( object src, ChangeEventArgs e);

Hier handelt es sich um Funktionszeiger auf beliebige Methoden, die kein Resultat zurückliefern und einen object- sowie einen ChangeEventArgs-Parameter besitzen. Einen solchen ‘Zeiger’ erzeugt Listing 1b in Zeile 17:

new ChangeEvent(OnChange);

Verwenden lässt sich dieses Delegate zum Beispiel wie folgt:

ChangeEvent ce = new ChangeEvent(OnChange);
ce(obj, arg); // Impliziter Aufruf von OnChange()

Der +-Operator kann Delegates ohne Resultat zu Multicast-Delegates kombinieren.

ChangeEvent ce1 = new ChangeEvent(OnChange1);
ChangeEvent ce2 = new ChangeEvent(OnChange2);
(ce1+ce2)(obj, arg); // Aufruf von OnChange1 UND OnChange2

Tatsächlich handelt es sich bei Delegates intern um Klassen, sodass sie sich auf die gleiche Weise wie Klassen definieren und nutzen lassen. Für die Definition der Schnittstelle des Ereignisproduzenten stellt C# in Form von Ereignissen (event) ein weiteres Sprachmerkmal zur Verfügung. Das Beispiel deklariert in der Klasse StockTicker (1a, Zeile 49):

public event ChangeEvent OnChangeEvent;

Den OnChange-Event nutzt der Client dazu, seine Delegates anzumelden, etwa in Zeile 17 von Listing 1b:

m_StockTicker.OnChangeEvent += new ChangeEvent( OnChange);

Ein Abmelden wäre dementsprechend durch den ‘-=’-Operator durchzuführen. Tritt ein entsprechendes Ereignis in StockTicker ein, erfolgt also die Änderung eines Aktienwerts in der Methode ChangeValue (1a, Zeile 67), ruft der Aktienticker automatisch alle Ereigniskonsumenten mittels

OnChangeEvent(this, args)

auf, sofern sich überhaupt welche angemeldet haben (OnChangeEvent != null). Dieser Aufruf bewirkt, dass alle für dieses Ereignis registrierten Clients über ihre Rückrufmethoden eine entsprechende Nachricht erhalten. Aus Umfangsgründen kann das Tutorial auf diesen Punkt nicht näher eingehen. Zumindest sei aber angemerkt, dass sich Delegates ebenfalls für asynchrone Aufrufe nutzen lassen.

Zur effizienten Nutzung von Multiprozessorsystemen, aber auch von Einprozessormaschinen ist die Verwendung von Threads essenziell. Leider sind Thread-Pakete betriebssystemabhängig. Daher ist es begrüßenswert, wenn Mechanismen und Bibliotheken existieren, die diese Unterschiede vor dem Entwickler verbergen. Gerade bei Java hat sich die Bereitstellung eigener Sprachmerkmale und Pakete bewährt. Kein Wunder, dass auch .NET Unterstützung für Multithreading integriert.

Mehr Infos

Listing 2

Damit sich Threads nutzen lassen, muss ein Programm das Paket System.Threading einbinden.

 1  using System;
2 using System.Threading;
3
4 namespace ThreadTest
5 {
6 class GlobalData {
7 int m_Value;
8 public int Value
9 {
10 get { lock(this) { return m_Value; } }
11 set { lock(this) { m_Value = value; } }
12 }
13 }
14 class Worker
15 {
16 GlobalData m_Global;
17 int m_WorkerNo;
18 public Worker(int val, GlobalData global) {
19 m_WorkerNo = val;
20 m_Global = global;
21 }
22 public void loop()
23 {
24 for (int i = 0; i < 10; i++)
25 {
26 Console.WriteLine("Current value: {0}",
27 m_Global.Value);
28 m_Global.Value = m_WorkerNo * 10 + i ;
29 Console.WriteLine("Current value: {0} by {1}",
30 m_Global.Value, Thread.CurrentThread.Name);
31 Thread.Sleep(100);
32 }
33 }
34 }
35 class ThreadClient
36 {
37 static void Main(string[] args)
38 {
39 GlobalData global = new GlobalData();
40 Worker w1 = new Worker(1, global);
41 Worker w2 = new Worker(2, global);
42 Thread t1 = new Thread(new ThreadStart(w1.loop));
43 Thread t2 = new Thread(new ThreadStart(w2.loop));
44 t1.Priority = ThreadPriority.Highest;
45 t1.Name = "Worker1";
46 t2.Name = "Worker2";
47 t1.Start();
48 t2.Start();
49 t1.Join();
50 t2.Join();
51 Console.WriteLine("All Done!");
52 Console.ReadLine();
53 }
54 }
55 }

Ein weiteres Beispiel demonstriert die Nutzung von Threads unter C# (Listing 2). Die Unterstützung von Threads verbirgt sich im Paket System.Threading. Im vorliegenden Fall kreiert das Hauptprogramm zwei Worker-Threads mit den Namen ‘Worker1’ und ‘Worker2’ und übergibt ihnen eine Referenz auf eine globale Datenstruktur des Typs GlobalData (Zeile 6), die lediglich eine ganzzahlige Zahl enthält. Die Erzeugung neuer Threads ist relativ einfach (Zeile 42):

Thread t1 = new Thread(new ThreadStart(w1.loop));

Der Anwender instanziiert die Klasse Thread und übergibt dem Konstruktor ein Delegate des Typs ThreadStart, wobei das Delegate eine ergebnis- und parameterlose Methode repräsentiert. Der eigentliche Start eines Thread erfolgt durch:

t1.Start();

Im Beispiel werden beide Threads durch Instanzen des Typs Worker (Zeile 14) implementiert, wobei die Thread-Eintrittsmethode, also die Methode, die der Thread durchläuft, sich in der Methode loop (Zeile 22) befindet.

class Worker {
...
public void loop() {
// do work
}
}

Der erste Thread überschreibt die globale Variable sequenziell mit 10, 11, 12 und so weiter und pausiert jeweils mittels Anweisung Thread.Sleep(100) für 100 ms (Zeile 31). Parallel dazu schreibt der zweite Thread nacheinander 20, 21, ... und pausiert ebenfalls für 100 ms nach jedem Schleifendurchlauf. Das Hauptprogramm wartet mit Thread.Join (Zeile 49) auf das Ende beider Worker-Threads und terminiert anschließend selbst. Durch Variation der Thread-Prioritäten (Zeile 44: Thread.Priority) und Wartezeiten lässt sich ein wenig der Ablauf steuern und das Verhalten des Scheduler studieren.

In der nebenläufigen Verarbeitung ist sicherzustellen, dass keine Race-Conditions entstehen und der Zugriff auf globale Daten synchronisiert und sequenziell erfolgt. Für diesen Zweck implementiert .NET eigene Synchronisationsmechanismen, deren Basis die Klasse System.Threading.Monitor darstellt. Mittels des Konstrukts lock(object){} lassen sich beispielsweise Mutexe definieren, die jeweils nur ein Thread zu einem bestimmten Zeitpunkt durchlaufen kann. Beispiel ist Zeile 10:

lock(this) { return m_Value; }

Sie nutzt die globale Datenstruktur selbst als Sperre. Wie in Java birgt die sorglose Nutzung von Threads Fallstricke, auf die die Literatur ausführlich eingeht [2].

Mehr Infos

Listing 3a und 3b

Dynamische Typabfrage: Über a.GetTypes()[0] kann der Client in Listing 3b auf Klassentypen zugreifen, die eine DLL wie in Listing 3a definiert.

Listing 3a

1  using System;
2
3 namespace Components {
4 public class MyComponent {
5 public MyComponent(){}
6 public virtual double algorithm(double number) {
7 return number * 2;
8 }
9 }
10 }

Listing 3b

1  using System;
2 using System.Reflection;
3
4 namespace ComponentClient {
5 class Client
6 {
7 static void Main(string[] args) {
8 Assembly a = Assembly.LoadFrom("Component.dll");
9 Type [] allTypes = a.GetTypes();
10 Type t = allTypes[0];
11 object o = Activator.CreateInstance(t);
12 MethodInfo mi = t.GetMethod("algorithm");
13 double d = (double) mi.Invoke(o, new object[]{21.0});
14 Console.WriteLine(d);
15 Console.ReadLine();
16 }
17 }
18 }

Mit Hilfe von Metainformationen lassen sich nicht nur Informationen dynamisch über Programme abfragen, sondern auch nutzen. Ein großer Teil der Mächtigkeit von Java resultiert aus dem dort verfügbaren Reflexions-paket. Dem wollte Microsoft nicht nachstehen und hat .NET einen ähnlichen Mechanismus beschert. Wieder ein praktisches Beispiel zur Erläuterung. Listing 3a zeigt den Quellcode für eine Bibliothekskomponente (Assembly) Component.DLL. Das dazugehörige lauffähige Clientprogramm (Listing 3b) lädt das Bibliotheks-Assembly zur Laufzeit in seinen Adressraum (also in die zugehörige Application Domain):

Assembly a = Assembly.LoadFrom("Component.dll");

und greift über a.GetTypes()[0] auf den dort definierten Klassentyp MyComponent zu. Mit Hilfe einer Aktivierungskomponente (Activator, Listing 3b, Zeile 11) lässt sich diese Klasse instanziieren:

object o = Activator.CreateInstance(t);

Anschließend liest das Listing die Typinformation zur Methode algorithm ein und ruft die Methode generisch mit dem Argument 21.0 auf.

MethodInfo mi = t.GetMethod("algorithm");
double d = (double) mi.Invoke(o, new object[]{21.0});

Das Ergebnis lautet 42 - wie sollte es anders sein. Mit Hilfe derartiger Reflexionsmechanismen ist nicht nur die Bereitstellung ausgefeilter Werkzeuge wie Typbrowser möglich, sondern zum Beispiel auch das dynamische Laden und Austauschen von Funktionen. Der Phantasie sind hier fast keine Grenzen gesetzt. Ein Warnhinweis soll an dieser Stelle aber nicht fehlen. Reflexionsmechanismen sollte man mit Bedacht nutzen und nur dann, wenn sie wirklich notwendig sind. Reflexive Programmierung ist nicht nur fehlerträchtig, sondern macht das daraus resultierende Programm schlecht les- und wartbar.

Neben dieser festverdrahteten Metainformation erlaubt .NET sogar die zusätzliche Bereitstellung benutzerdefinierter Metainformation durch so genannte Attribute. Ein Beispiel:

[AuthorIs("Michael")]
class MyClass {
...
}

Das fiktive Attribut AuthorIs zur Angabe des jeweiligen Codeautors lässt sich ebenfalls dynamisch abfragen. Die benutzerdefinierte Integration neuer Attribute erfolgt über Attributklassen:

[AttributeUsage(AttributeTargets.All)]
public class AuthorIsAttribute : Attribute {
private string m_Name;
public AuthorIsAttribute(string name) {
m_Name = name; }
}

Die Attributklasse ist selbst rekursiv mit einem Attribut deklariert. Dies spezifiziert, dass AuthorIs für beliebige Elemente anwendbar sein soll (AttributeTargets.All). Möglich wären auch andere Einstellungen wie AttributeTargets.Class, um die Anwendung des Attributs auf Klassen zu beschränken.

Im Allgemeinen laufen .NET-Programme unter vollständiger Kontrolle des Garbage Collector. Dieser entfernt nicht mehr benötigte Objekte vom Heap und verschiebt die anderen gegebenenfalls, um die Speicherausnutzung kompakter zu gestalten. Der Zugriff auf Objekte erfolgt über Variablen, die sich auf Stack- oder Heap-Objekte beziehen. Direkt über Adressen zuzugreifen, ist nicht gestattet. Wer mit diesen Einschränkungen nicht leben will, kann ‘unsicheren Code’ verwenden:

class TestUnsafeData {
class AClass {
public int i;
public override string ToString()
{ return i.ToString(); }
}
static unsafe void Main(string[] args) {
int x = 7;
int *y = &x;
*y = 12;
AClass a = new AClass();
fixed (int *z = &a.i) { *z = 42; }

Console.WriteLine(x);
Console.WriteLine(a);
// Ergebnis: 12 42
Console.ReadLine();
}
}

Die als unsafe spezifizierte Methode erlaubt in ihrem Rumpf Zeiger und Adressarithmetik im Stil von C++. Ein Problem ergibt sich bei Zeigern auf Heap-Objekte, die der Garbage Collector zur Laufzeit verschieben kann. Um solche Zeiger trotzdem einsetzen zu können, existiert das Sprachkonstrukt fixed. Im Beispiel zeigt die Zeigervariable z, die nur innerhalb des fixed-Blocks gültig ist, auf ein Datenfeld einer Klasseninstanz. Innerhalb des fixed-Blocks ist garantiert, dass der Garbage Collector die Instanz a im Speicher fixiert lässt und nicht verschiebt.

Zugriffe auf nativen Code, der nicht der Kontrolle der CLR (Common Language Runtime) unterliegt, also auf Methoden in konventionellen DLLs, unterstützt C# mittels des so genannten P/Invoke-Mechanismus (Platform Invoke):

class PInvokeTest {
[DllImport("user32.dll")]
static extern int MessageBoxA(int hWnd, string m, string c, int t);
static void Main(string[] args) {
MessageBoxA(0, "Hello DLL", "My Window", 0);
}
}

Im obigen Code greift die Main-Methode auf die als extern definierte DLL-Methode MessageBoxA zu. Für den wichtigen Sonderfall COM/COM+ bietet .NET entsprechende Werkzeuge, mit denen sich COM+-Komponenten über so genannte RCWs (Runtime Callable Wrapper) zu .NET-Komponenten ‘wrappen’ oder sich umgekehrt .NET-Komponenten über CCWs (COM Callable Wrappers) als COM-Komponenten nutzen lassen. Wichtig sind diese Wrapper bei Funktionen, die es in .NET nicht gibt, zum Beispiel beim Nutzen von Transaktionsmonitoren wie MTS/COM+. Ausführliche Details zu dieser Thematik enthält der Band ‘Professional C#’ [3].

Im ersten Teil des Tutorials war bereits von Assemblies die Rede. Dort wurden sowohl die Struktur als auch die Klassifizierung in Shared und Private Assemblies erläutert. Letztere sind im Verzeichnis der Applikation oder in einem der Subverzeichnisse anzutreffen. Die Laufzeitumgebung versucht dementsprechend, Assemblies erst im lokalen Verzeichnis aufzuspüren und sucht danach in Unterverzeichnissen weiter - das so genannte Probing. Da jede Anwendung ihre privaten Assemblies mitbringt, ist das Problem von Namenskollisionen irrelevant.

Global verfügbare Assemblies befinden sich hingegen in der Regel in einem zentralem Systemverzeichnis, dem Global Assembly Cache. Dessen Inhalt lässt sich mit gacutil abfragen (gacutil - l). Um eigene Shared Assemblies zu erzeugen, ist zunächst das Generieren eines Schlüsselpaars mit dem Tool sn erforderlich: sn -k mykey.snk. Dem Übersetzer werden Ort des Schlüsselpaares über spezielle Assembly-Attribute mitgeteilt. In Visual Studio .NET ist zu diesem Zweck zu jedem Projekt eine eigene Datei AssemblyInfo.cs vorhanden. Bei Nutzung des Kommandozeilencompilers ist der Namensraum System.Reflection per Include-Anweisung einzubinden und am Anfang des Codes folgendes zu spezifizieren:

[assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyKeyFile( @"<rel. oder abs. pfad>\mykey.snk")]

Das erste Attribut spezifiziert die Version mit 1.0 (die dritte Ziffer definiert den Build und die vierte die Release). Das Attribut AssemblyKeyFile gibt die Position des Assembly im Dateisystem an.

(Bemerkung am Rande: Das Zeichen ‘@’ am Anfang des String ermöglicht die Verwendung von Sonderzeichen wie ‘\’ in Zeichenketten. Statt ‘\\pfad\\file’ lässt sich kürzer @’\pfad\file’ schreiben.)

Der C#-Übersetzer extrahiert beim Build-Prozess den privaten Schlüssel, ermittelt einen Hash-Wert über das Assembly und schreibt sowohl den Hash als auch den öffentlichen Schlüssel ins Assembly. Dadurch lassen sich zum einen Manipulationen verhindern und zum anderen erhält das Assembly dadurch einen eindeutigen Namen (strong name). In Client-Assemblies, die dieses Shared Assembly nutzen, integriert der Compiler übrigens aus Platzgründen einen über den öffentlichen Schlüssel errechneten Hash-Wert, nicht den Schlüssel selbst. Mit dem Werkzeug ildasm lassen sich die die Inhalte von Assemblies untersuchen (siehe Abbildung 1).

Wenn das Assembly signiert und fertig übersetzt vorliegt, muss der Programmierer es nur noch mit gacutil -i <assembly> in den Global Assembly Cache installieren. Auch das Entfernen installierter Shared Assemblies ist möglich.

Am Ende dieses zweiten Tutorial-Teils soll der Blick auf eine Eigenschaft gerichtet werden, mit der Microsoft besonders für sein neues Framework wirbt - die Programmierprachenunabhängigkeit. Dank Übersetzung in IL (Intermediate Language) und Kontrolle durch das VES (Virtual Execution System) ist ein ‘Sprach-Mischmasch’ kein Problem mehr. Sogar COBOL- und Eiffel-Compiler existieren mittlerweile für .NET. Endlose Religionskriege in Teeküchen und Diskussionsforen über die beste Programmiersprache neigen sich somit ihrem Ende zu - möchte man meinen. Don Box würde an dieser Stelle ‘.NET is Love’ verlautbaren. Aber zurück zu den Fakten.

Wie üblich soll die Veranschaulichung pragmatisch erfolgen. Eine Managed-C++-Bibliothek definiert eine einfache Klasse, deren einzige Methode eine C#-Unterklasse überschreibt. Genutzt wird das Ganze schließlich aus VB (Listing 4). Die Ausgabe des VB-Hauptprogramms lautet dementsprechend wenig überraschend ‘Hallo__CSHARP__’.

Mehr Infos

Listing 4

Kein Turmbau zu Babel: Die Intermediate Language (IL) erlaubt es, unterschiedliche Programmiersprachen gleichzeitig zu nutzen.

// C++:
#pragma once
using namespace System;
namespace CPPBase {
public __gc class CPPBaseClass {
public: virtual System::String __gc*
Echo(System::String __gc *s);
};
}
System::String __gc * CPPBase::CPPBaseClass::
Echo(System::String __gc *s) {
return s;
}

// C#:
using System;
using CPPBase;
namespace CSharpDerived {
public class CSharpDerivedClass : CPPBaseClass {
public override string Echo(string s) {
return base.Echo(s) + "__CSHARP__";
}
}
}

` Visual Basic.NET
Module Module1
Sub Main()
Dim obj As New CSharpDerived.CSharpDerivedClass()
Console.WriteLine(obj.Echo("Hallo"))
Console.ReadLine()
End Sub
End Module

Kurzes Resümee: C# erweist sich hier als ideale Programmiersprache, VB.NET unterscheidet sich nur noch syntaktisch von C#. ‘C++ with Managed Extensions’ bietet zwar die größte Flexibilität, beinhaltet aber auch die meisten Fallstricke.

Ob Projektleiter allerdings damit glücklich würden, wenn jedes Teammitglied seine Lieblingssprache verwendet, sei an dieser Stelle dahingestellt.

Der zweite Teil des Kurses hat detaillierte Blicke auf fortgeschrittene Eigenschaften von C# und .NET geworfen. Typisierte Ereignisse und Funktionszeiger erlauben zum Beispiel die elegante Umsetzung des Observer-Patterns für Ereignismeldungen. Eine effiziente Nutzung von Parallelität bietet sich durch Threads und Synchronisationsmechanismen an. Durch die Verfügbarkeit von Typinformation sind die Entwicklung ausgefeilter Werkzeuge sowie die Nutzung dynamischer Programmiertechniken wie - auf Neudeutsch - On-Demand-Activation oder Late-Binding möglich. Mittels spezieller Sprachkonstrukte wie unsafe und fixed lassen sich Zeiger- und Zeigerarithmetik à la C++ nutzen. Interoperabilität zwischen .NET-Sprachen regelt die Common Language Runtime, Interoperabilität mit nativem Nicht-IL-Code ist durch Platform Invoke zu bewerkstelligen, wobei für COM-Interoperabilität zusätzliche Werkzeuge zur Erzeugung von Wrapper-Klassen existieren. Assemblies lassen sich durch Schlüssel signieren und global zur Verfügung stellen.

Aus Platzgründen kann das Tutorial weitere Details wie Sicherheitsfunktionen, Konfigurationsaspekte oder Nutzung von Compilerdirektiven nicht ansprechen. Dazu findet sich in der Literatur eine detaillierte Abhandlung [3].

Der dritte und letzte Teil des Kurses legt den Fokus auf die .NET-Anwendungsprogrammierung mit den entsprechenden Framework-Bibliotheken.

Michael Stal
ist Senior Principal Engineer bei der Corporate Technology der Siemens AG und leitet das Kompetenzfeld Middleware & Application Integration. Er ist Chefredakteur der Zeitschrift Java Spektrum sowie Koautor der Buchreihe ‘Pattern-Oriented Software Architecture’.

[1] E. Gamma, R. Helm, R. Johnson, J. Vlissides; Design Patterns - Elements of Reusable Object-Oriented Software; Addison-Wesley, Reading 1995

[2] D. Schmidt, M. Stal, H. Rohnert, F. Buschmann; Pattern-Oriented Software Architecture; Volume 2; Patterns for Concurrent and Networked Objects; Wiley & Sons, Chichester 2000

[3] Robinson, Cornes, Glynn, Harvey, McQueen, Moemeka, Nagel, Skinner, Watson; Professional C#; Wrox Press, Birmingham 2001

[4] M. Stal; Ton angebend; C#- und .NET-Tutorial, Teil 1; iX 12/2001, S. 122 ff.

Mehr Infos

iX-TRACT

  • Für die Programmierung benutzerdefinierter Event-Handler stellt .NET die Programmkonstrukte Events und Delegates zur Verfügung.
  • Die Thread-Unterstützung erlaubt die effiziente Nutzung von Multiprozessorsystemen.
  • Wer seine Programme der Kontrolle des Garbage Collector entziehen will, kann mit ‘unsicherem Code’ über Variablen direkt auf Stack- und Heap-Objekte zugreifen.
  • Zugriff auf DLLs, die nicht der Kontrolle der Common Language Runtime unterliegen, erlaubt der P/Invoke-Mechanismus.
  • Durch die Übersetzung in die Intermediate Language (IL), die das Virtual Execution System (VES) auführt, ist es theoretisch möglich, beliebige Programmiersprachen innerhalb eines Projekts zu nutzen.

Tutorial Teil 1

Tutorial Teil 3

Mehr Infos

Übungsaufgabe:

Erweitern Sie den Aktienticker um folgende Funktionen:

  • Ein zusätzlicher Filtermechanismus in Gestalt eines Adapters, der seinerseits auf Events und Delegates basiert, soll dafür sorgen, dass Clients explizit angeben können, an welchen Aktienwerten sie konkret interessiert sind. Sie erhalten in dem Fall nur dann Rückrufe, wenn sich die entsprechenden Aktienwerte verändert haben.
  • Parallelisieren Sie die Clients in diesem Beispiel und erweitern Sie sie, wo nötig, mit Synchronisationsmechanismen.

Die Auflösung ist hier zu finden. (ka)