Delegates vs. Events

February 28, 2008 at 11:22 AMAndre Loker

Manche Fragen tauchen mit schöner Regelmäßigkeit in Foren etc. auf. Eine davon lautet:

Was ist der Unterschied zwischen Ereignissen (Events) und Delegaten (Delegates)?

Beispiel:

   1: delegate void MyDelegate( string message );
   2:  
   3: class MyClass {
   4:     public MyDelegate MyDelegate;
   5:     public event MyDelegate MyEvent;
   6: }

MyClass.MyDelegate ist ein Delegate, MyClass.MyEvent entsprechend ein Event. Der Unterschied zwischen den beiden besteht hauptsächlich in den Zugriffsrechten außerhalb der Klasse MyClass.

Wir können drei Operationen festhalten, die für delegates bzw. events wichtig sind:

  • Hinzufügen/Entfernen von Event-Handlern (mit den += und -= Operatoren)
  • Direktes Zuweisen eines Wertes (mit dem = Operator)
  • Auslösen des Events/aufrufen der Event-Handler (mit dem ()-Operator)

Der Unterschied zwischen Event und Delegate lässt sich mit folgender Tabelle leicht veranschaulichen:

Operation Event Delegate
Innerhalb MyClass    
  Hinzufügen/Entfernen mit += und -= erlaubt erlaubt
  Zuweisen mit = erlaubt erlaubt
  Auslösen mit () erlaubt erlaubt
Außerhalb MyClass    
  Hinzufügen/Entfernen mit += und -= erlaubt erlaubt
  Zuweisen mit = nicht erlaubt erlaubt
  Auslösen mit () nicht  erlaubt erlaubt

 

Mit "innerhalb" MyClass sind Methoden gemeint, die MyClass selbst definiert. Von MyClass abgeleitete Klassen werden in dieser Tabelle wie "außerhalb MyClass" behandelt.

Code-Beispiel:

   1: using System;
   2:  
   3: delegate void MyDelegate( string message );
   4:  
   5: class MyClass {
   6:     public MyDelegate MyDelegate;
   7:     public event MyDelegate MyEvent;
   8: }
   9:  
  10: class Program {
  11:     static void Main() {
  12:         MyClass mc = new MyClass();
  13:  
  14:         // DELEGATES
  15:         // Subscribe
  16:         mc.MyDelegate += HandleTheEvent; // OK
  17:         // Unsubscribe
  18:         mc.MyDelegate -= HandleTheEvent; // OK
  19:         // Clear all subscribers
  20:         mc.MyDelegate = null; // OK
  21:         // Invoke subscribers
  22:         mc.MyDelegate( "Hello, world!" ); // OK
  23:  
  24:         // EVENTS
  25:         // Subscribe
  26:         mc.MyEvent += HandleTheEvent; // OK
  27:         // Unsubscribe
  28:         mc.MyEvent -= HandleTheEvent; // OK
  29:         // Clear all subscribers
  30:         mc.MyEvent = null; // NOT ALLOWED, Cpmpiler error
  31:         // Invoke subscribers
  32:         mc.MyEvent( "Hello, world!" ); // NOT ALLOWED, Compiler error
  33:     }
  34:  
  35:     private static void HandleTheEvent( string message ) {
  36:         Console.WriteLine( message );
  37:     }
  38: }

Zusammengefasst:

Der wesentliche Unterschied bei der Handhabung von Delegates und Events besteht darin, dass außerhalb der Klasse, die das Event bzw. den Delegaten definiert, das direkte Zuweisen und das Auslösen eines Events nicht erlaubt ist, während beides bei einem Delegaten gesstattet ist.

Oft wird nun gefragt, was man nehmen sollte: Events oder Delegates. Wenn man Ereignisse anbieten möchte, wird man verständlicherweise zu Events greifen. Ein öffentlicher Delegat bricht die Kapselung der Klasse viel zu sehr auf. Ein Event hingegen bietet eine wohldefinierte begrenzte Schnittstelle zur Außenwelt an. Zudem erlauben Events mehr Feinkontrolle, wie der nächste Abschnitt zeigt.

Details

Es gibt - gerade in technischer Hinsicht - natürlich einen größeren Unterschied zwischen Events und Delegaten. Dazu muss man wissen, wie ein Event für die CLR aussieht. Bei der Definition des Events

   1: public event MyDelegate MyEvent;

erzeugt der C# Compiler in etwa folgenden Code:

   1: private MyDelegate _MyEvent;
   2:  
   3: public void add_MyEvent(MyDelegate value)
   4: {
   5:     this.MyEvent = (MyDelegate) Delegate.Combine(this.MyEvent, value);
   6: }
   7:  
   8: public void remove_MyEvent(MyDelegate value)
   9: {
  10:     this.MyEvent = (MyDelegate) Delegate.Remove(this.MyEvent, value);
  11: }

Hinweis: In Wirklichkeit haben das Feld und das Event den selben Namen (MyEvent). Zur Veranschaulichung verwende ich aber _MyEvent für das Feld und MyEvent für das Event.

In Wahrheit erzeugt die Definition des Events also ein privates Feld vom Typ des Delegaten und zwei öffentliche Wrapper-Methoden, um sich bei dem Delegaten an- bzw. abzumelden. Die += und -= Operator für das Event werden vom Compiler zu Aufrufen der add_MyEvent und remove_MyEvent Methoden kompiliert. Das erklärt, warum dies die einzigen beiden Operationen sind, die außerhalb der MyClass-Klasse erlaubt sind. Da _MyEvent private ist, erklärt sich auch, warum selbst von MyClass abgeleitete Klassen lediglich += und -= verwenden dürfen.

Auch das Auslösen des Events ist zunächst syntaktischer Zucker:

   1: MyEvent( "foo" );

wird zu

   1: _MyEvent( "foo" );

In manchen Fällen möchte man das Standardverhalten des C#-Compilers (erstellen eines privaten Feldes des Delegaten-Types und Erzeugen der einfachen add und remove-Methoden) überschreiben. In der .NET Klassenbibliothek wird das z.B. bei den Windows-Forms oft gemacht, da ein Control viele Events hat, die bei weitem aber nicht alle verwendet werden. Würde man hier das Standardverhalten verwenden, würde für jedes Event ein Feld angelegt, das entsprechend Platz pro Instanz belegt.

Um das Verhalten zu überschreiben, kann man in C# für ein Event explizit die add- und remove-Methoden angeben. Hier ein komplexeres Beispiel, wie man mehrere Events zentral handhaben kann, indem man mit Hilfe eines Dictionary-Feldes die Buchführung für Events selber übernimmt:

   1: class MyClass {
   2:     // keys to identify events in the dictionary
   3:     private static readonly object Event1Key = new object();
   4:     private static readonly object Event2Key = new object();
   5:     private static readonly object Event3Key = new object();
   6:     private static readonly object Event4Key = new object();
   7:     // stores all active events
   8:     private readonly Dictionary<object, Delegate> events = new Dictionary<object, Delegate>();
   9:  
  10:     public event EventHandler Event1 {
  11:         add { AddHandler( Event1Key, value ); }
  12:         remove { RemoveHandler( Event1Key, value ); }
  13:     }
  14:  
  15:     public event EventHandler Event2 {
  16:         add { AddHandler( Event2Key, value ); }
  17:         remove { RemoveHandler( Event2Key, value ); }
  18:     }
  19:  
  20:     public event EventHandler Event3 {
  21:         add { AddHandler( Event3Key, value ); }
  22:         remove { RemoveHandler( Event3Key, value ); }
  23:     }
  24:  
  25:     public event EventHandler Event4 {
  26:         add { AddHandler( Event4Key, value ); }
  27:         remove { RemoveHandler( Event4Key, value ); }
  28:     }
  29:  
  30:     // add a handler using the dictionary
  31:     private void AddHandler( object key, Delegate value ) {
  32:         Delegate old;
  33:         if ( events.TryGetValue( key, out old ) ) {
  34:             // Handler exists, combine old with new
  35:             events[ key ] = Delegate.Combine( old, value );
  36:         } else {
  37:             // Handler does not exist
  38:             events[ key ] = value;
  39:         }
  40:     }
  41:  
  42:     // remove a handler using the dictionary
  43:     private void RemoveHandler( object key, Delegate value ) {
  44:         Delegate old;
  45:         if ( events.TryGetValue( key, out old ) ) {
  46:             // Handler exists, remove it
  47:             events[ key ] = Delegate.Remove( old, value );
  48:         }
  49:     }
  50:  
  51:     // raises a specific event
  52:     protected void Raise( object key, object sender, EventArgs e ) {
  53:         Delegate handler;
  54:         if ( events.TryGetValue( key, out handler ) ) {
  55:             // assume we use a delegate format similar to EventHandler, 
  56:             // i.e.: it expects (object sender, EventArgs e) as arguments
  57:             handler.DynamicInvoke( sender, e );
  58:         }
  59:     }
  60:  
  61:     // Some method that raises an event
  62:     public void SomeMethod() {
  63:         // ...
  64:         // Raise Event1
  65:         Raise( Event1Key, this, EventArgs.Empty );
  66:         // Raise Event3
  67:         Raise( Event3Key, this, EventArgs.Empty );
  68:     }
  69: }

Wichtig hierbei:

  • Es macht Sinn, das Hinzufügen/Entfernen der Handler zum/vom Dictionary in eigene Methoden auszulagern (AddHandler/RemoveHandler)
  • Man kann Events, die eigene add/remove-Methoden implementieren nicht mehr über EventName() auslösen. Statt dessen muss hier der entsprechende Delegat im Verzeichnis gefunden und aufgerufen werden. Da das Verzeichnis hier sehr allgemeingültig ist (die Delegaten werden als Delegate-Referenzen gespeichert), verwende ich hier Delegate.DynamicInvoke. Wäre der Typ des Delegaten klar definiert (z.b. EventHandler), könnte auch der normale Aufruf mittels () verwendet werden. Die Variante mit DynamicInvoke hat den Vorteil, dass unterschiedlichste Eventhandler-Delegaten verwendet werden, solange sie alle vergleichbare Parameter haben. Das erklärt übrigens auch, warum Microsoft das Format HandlerDelegate(object sender, EventArgs e) forciert.
  • Die Verwendung von Dictionary ist sicherlich nicht die platzsparendste Variante, dazu hat die Klasse einfach zu viel verwaltungstechnischen Overhead. Das .NET Framework bietet für diese Zwecke eine sehr schlanke Klasse an: System.ComponentModel.EventHandlerList (Beispiel). Die Variante mit Dictionary soll nur veranschaulichen, wie das Konzept grundsätzlich funktioniert.

Die Verwendung der Events ändert sich dadurch aber nicht:

   1: class Program {
   2:     private static void Main() {
   3:         MyClass mc = new MyClass();
   4:         // use as always
   5:         mc.Event1 += MyClassHandler;
   6:         mc.Event2 += MyClassHandler;
   7:         mc.Event3 += MyClassHandler;
   8:         mc.Event4 += MyClassHandler;
   9:         mc.SomeMethod();
  10:     }
  11:  
  12:     private static void MyClassHandler( object sender, EventArgs e ) {
  13:         Console.WriteLine( "Event raised" ); 
  14:     }
  15: }

Posted in: C# | FAQ

Tags: , ,

Webcam verhindert Update von Thunderbird

February 27, 2008 at 10:14 AMAndre Loker

Bei mir brach heute das Update von Thunderbird auf Version 2.0.0.12 mit folgender Meldung ab:

Software-Update fehlgeschlagen. Eine oder mehrere Dateien konnte nicht aktualisiert werden. Bitte schließen Sie alle anderen Anwendungen, und stellen Sie sicher, dass Sie ausreichende Benutzerrechte besitzen, um Dateien zu verändert. Starten Sie Thunderbird dann nochmals, um es erneut zu versuchen.

Nach diversen fehlgeschlagenen Versuchen bin ich in einem Forum auf den Tipp gestoßen, dass ein WebCam-Treiber womöglich Dateien von Thunderbird blockiert. Und tatsächlich, nachdem ich den Prozess "Webcam10.exe" mittels Taskmanager beendet hatte, funktionierte auch wieder das Update. Man muss nur mal drauf kommen...

Posted in: Tools

Tags:

Tabellen mit bigint-Prim&auml;rschl&uuml;ssel in Access verkn&uuml;pfen

February 26, 2008 at 3:08 PMAndre Loker

Für ein Projekt benötigten wir folgendes Szenario: eine SQL Server Datenbank sollte mittels ODBC für Access verfügbar gemacht werden. Eine der Tabellen sollte dann in Access verwendet werden. Dazu bietet Access zwei Optionen:

  • Daten aus der Tabelle importieren. Hierbei wird der Inhalt der Tabelle in eine lokale Access-Datenbank kopiert
  • Tabelle mit der Access Datenbank verknüpfen. Hier wird lediglich eine Verknüpfung mit der Tabelle im SQL Server hergestellt, sodass die Daten immer aktuell sind.

Die erste Möglichkeit funktionierte auch tadellos, beim Verknüpfen der Tabelle trat jedoch ein Problem auf: sämtliche Spalten aller Reihen enthielten statt der erwarteten Werte nur ein "#gelöscht" (bzw. #deleted oder #verwijderd, je nach Sprachversion). Nach einigem Suchen sind wir darauf gestoßen, dass das Problem darin bestand, dass die Tabelle als Primärschlüssel einen bigint Datentypen verwendete. Damit kam Access bzw. der JET Treiber nicht zurecht und interpretiert den Schlüssel als Binärdaten bzw. als Text. KB321901 erwähnte dieses Problem bereits im Juni 2004. Warum Microsoft es bisher nicht geschafft hat, dieses Problem zu lösen, ist mir schleierhaft.

Auf der Suche nach einem Workaround wurde ich in einem Forenbeitrag fündig: man erstellt einfach eine Sicht (View) der gewünschten Daten und konvertiert in dieser Sicht den Primärschlüssel in einen varchar.

   1: CREATE VIEW vwTheTable AS 
   2: SELECT CAST(ID AS varchar(20)) AS AccessID, *
   3: FROM TheTable

Die Sicht kann man dann in Access verknüpfen. Wird man dabei nach dem Primärschlüssel gefragt, so wählt man die Spalte AccessID aus. Lesezugriff und Updates sollten damit funktionieren. Inserts haben wir bisher nicht getestet. Sollte Access dennoch die alte ID Spalte als Primärschlüssel auswählen, kann man die Spalte beim Erstellen der Sicht weglassen.

Posted in: Databases

Tags: ,

Effizienz primitiver Datentypen im ControlState

February 25, 2008 at 10:10 PMAndre Loker

ASP.NET 2 bietet neben dem ViewState auch den so genannten ControlState an, um Control-Zustände über Postbacks hinweg zu speichern. In der Standardeinstellung wird der ControlState wie der ViewState in das versteckte Feld "__VIEWSTATE" der Seite serialisiert. Informationen dazu, wie man ControlStates verwendet, gibt es genügend im Internet.

Durch Zufall bin ich heute aber auf eine interessante Tatsache gestoßen. Natürlich sollten die Daten des ControlStates so klein wie möglich gehalten werden. In meinem konkreten Fall musste das Control lediglich die ID aus einer Datenbank speichern. Der Wert war vom Typ Int64, was 8 bytes zur Laufzeit entspricht. Ich erwartete also etwas mehr als diese 8 bytes im ControlState. Zur Kontrolle habe ich mir trace.axd zur Hilfe genommen, um die Größe des ControlStates zu überprüfen - und war doch ziemlich überrascht, dass dieser eine Int64 sage und schreibe 132 bytes veranschlagte. Das schien mir doch reichlich viel und veranlasste mich dazu, genauer zu hinterfragen, warum so der Wert so groß ist.

Versuchsweise verwendete ich statt des Int64 einen "gewöhnlichen" Int32 und erhielt als ControlState Größe nunmehr lediglich 8 bytes. Mir wurde dann relativ schnell klar, dass die Wahl des Datentyps von entscheidender Bedeutung ist. Ich habe also eine Testreihe gestartet, in der ich verschiedenste primitive Datentypen als ControlState verwendete. Dabei wurde auch deutlich, dass nicht nur der Datentyp eine Rolle spielte, sondern auch der Wert(!). Kleine Integer wurden offenbar kompakter serialisiert als große.

Ergebnisse

Ganzzahlige und Fließkommadatentypen

Die erste "Disziplin" ist das Speichern einfacher numerischer Datentypen, sowohl ganzzahlige Typen als auch Fließkommatypen. Getestet habe ich einmal den Wert "0" für jeden Datentyp und den höchsten Wert, welcher durch das konstante Feld MaxValue bei allen Typen angegeben ist.

Datentyp ControlState:
Wert 0
ControlState:
MaxValue
byte 8 8
sbyte 132 136
short 12 12
ushort 132 140
int 8 16
uint 132 144
long 132 156
ulong 132 160
float 12 12
double 20 20
decimal 136 172

 

Ergebnis: Byte, short, int float und double werden gesondert behandelt und sehr effizient gespeichert. Alle vorzeichenlosen Ganzzahlen (Ausnahme: bytes - dort sind es die vorzeichenbehafteten) sowie long, ulong und decimal sind "teuer". Für sie besteht offensichtlich keine effiziente Serialisierung im Framework.

 

Strings und Booleans

Strings und Boolsche Werte.

Wert ControlState
true 8
false 8
"test" 16
string.Empty 8

 

Ergebnis: sowohl Boolsche Werte als auch Strings werden sehr effizient serialisiert.

 

arrays

Falls man mehrere Werte als ControlState speichern möchte, wäre es ggf. interessant, diese als Array zu serialisieren. Ich habe einfache object-Arrays getestet als auch einige spezialisierte arrays (byte und int).

Array inhalt ControlState:
Wert 0 bzw String.Empty
ControlState:
MaxValue bzw. "test
"
object[] { int } 12 20
object[] { int, int } 16 28
object[] { int, int, int } 16 36
object[] { int, int, int, int } 16 44
object[] { byte } 16 16
byte[] { byte } 132 132
int[] { int } 12 20
string[] {string } 12 16

 

Ergebnis: object-Arrays scheinen sehr kompakt gespeichert zu werden. Ebenso sind int und string Arrays sehr effizient. Andere Arraytypen scheinen nicht gesondert behandelt zu werden.

 

8 bytes als speichern

In meinem konkreten Beispiel möchte ich einen Int64 als ControlState speichern. Die folgende Tabelle zeigt, wie ich dieses 8 bytes ggf. auch als ein Array von kleineren Zahlenwerten speichern könnte, indem ich die 8 bytes aufteile.

Array ControlState:
Wert 0
ControlState:
MaxValue
object[] { 8x byte } 32 32
object[] { 4x short } 28 28
object[] { 2x int } 16 28

 

Ergebnis: wie schon zuvor siegt der Int32-Typ. Doch selbst, wenn der Int64 Wert als ein Array von 8 bytes gespeichert wird, beträgt die Größe höchstens ein Viertel im Vergleich zum serialisierten Int64

 

Pair und Triplet

Als Alternative zum Array bieten sich für ControlStates, die aus zwei oder drei Werten bestehen, die Typen Pair und Triplet im System.Web.UI Namespace an. Die folgende Tabelle zeigt die ControlState Größe bei Verwendung des Pairs oder Triplets und zum Vergleich dazu noch einmal gleichwertige Arrays mit zwei bzw. drei Werten.

Inhalt ControlState:
Wert 0
ControlState:
MaxValue
Pair(int, int) 12 24
object[] { int, int } 16 28
Triplet(int, int, int) 12 32
object[] { int, int, int } 16 36

 

Ergebnis: offensichtlich sind Pair und Triplet nochmal jeweils 4 bytes kleiner als ihr Array-Pendant.

Fazit

Es ist offensichtlich, dass einige Datentypen gesondert serilisiert werden. Mit einer klugen Auswahl des Datentypes, kann die Größe des ControlStates dramatisch verkleinert werde, je nach Fall zwischen 75% und 90%.

Einige Richtlinien, die man aus den Tabellen entnehmen kann:

  • int (Int32) ist generell der bevorzugte Datentyp für Ganzzahlen.
  • Im ControlState zu empfehlen sind nur folgende primitive Typen: byte, short, int, float, double, bool und string.
  • Um zwei oder drei Werte zu speichern, sollten Pair bzw. Triplet verwendet werden
  • Bei mehr als drei Werten können Arrays verwendet werden. In der Regel sollte das ein object[] Array sein. Bei int und string sind int[] respektive string[] gleich effizient gegenüber object[].
  • 64 bit Integer sollten zum Speichern mittels Bitmaske in 2 32 bit Integer geteilt und als Pair gespeichert werden
  • sbyte, ushort und ulong sollten zunächst auf Bitniveau in die effizienten Pendants byte, short und long gebracht werden.

Posted in: ASP.NET

Tags: , ,

Auch Nerds fahren Auto

February 25, 2008 at 9:30 AMAndre Loker

Ohne Worte.

unix

Posted in: Off topic

Tags: