(0) exportieren Drucken
Alle erweitern
Erweitern Minimieren

Prädikate und Aktionen

Veröffentlicht: 25. Sep 2006

Von Ken Getz

Ich bin nicht faul, aber es ärgert mich doch, wenn ich manuell durch alle Mitglieder einer Sammlung gehen und mit jedem Mitglied eine Aktion durchführen muss. Ich wünschte, ich könnte der Sammlung einfach angeben, was mit jedem Mitglied zu tun ist, und sie dann automatisch die Elemente durchlaufen lassen. Und jetzt stellen Sie sich das mal vor! Ich habe kürzlich ein wenig in Microsoft ® .NET Framework herumgesucht und genau für dieses und andere Array- und Listenprobleme die richtige Lösung gefunden.

Es hat sich herausgestellt, dass die Klassen System.Array und System.Collections.Generic.List von .NET Framework 2.0 jeweils eine Reihe von Methoden wie z. B. Find, FindAll und FindLast bieten, mit denen Sie verhindern, dass Sie einen Code schreiben, der alle Elemente eines Arrays oder einer Liste einzeln durchläuft, um eines oder mehrere der von Ihnen gesuchten Elemente zu finden. Sie können also durch eine ganze Datenstruktur gehen und festlegen, ob jedes einzelne Element einen Satz von Kriterien erfüllt, ohne dass Sie den Standardcode so schreiben müssen, dass er manuell jede Zeile durchläuft. Weil das Prädikat – das Thema dieser Rubrik – einfach die Adresse einer aufrufbaren Methode ist, die in Wirklichkeit jedes Element einer Sammlung bestätigt oder ablehnt, können Sie zur Laufzeit die Suchkriterien einfach ändern.

Laden Sie den Code zu diesem Artikel herunter: AdvBasics2006_09.exe (178 KB)

Auf dieser Seite

Einzelheiten zu Prädikaten
System.Predicate und System.Action
System.Converter
Weiterführende Informationen
Der Autor

Einzelheiten zu Prädikaten

Prädikate nutzen die neuen generischen Features von .NET Framework 2.0 (die in früheren Versionen von Framework nicht vorhanden waren, was diese Art von Lösung schwieriger machte). In der Dokumentation von .NET Framework wird der Delegat System.Predicate formell wie folgt definiert:

Public Delegate Function Predicate(Of T)(obj As T) As Boolean

In der Praxis gibt diese Definition an, dass eine Funktion, die als ein Prädikat fungiert, einen einzelnen Wert als Parameter nehmen muss, der denselben Typ aufweisen muss wie die Daten des Arrays oder der Liste, mit dem bzw. der die Funktion arbeitet; zudem muss die Funktion einen booleschen Wert zurückgeben. Der Rückgabewert gibt an, ob der an die Methode geleitete Wert Ihre spezifischen Kriterien für eine Inklusion erfüllt.

Hier ein einfaches Beispiel: Stellen Sie sich vor, dass Sie ein Array von Bytes mit zufälligen Zahlen gefüllt haben und ein Array abrufen möchten, dass alle Werte unter 50 enthält. Sie könnten nun also jedes einzelne Element im Original-Array durchlaufen, jeden Wert mit 50 vergleichen und die geeigneten Werte in ein neues Array kopieren. Oder Sie könnten stattdessen die Methode Array.FindAll aufrufen und das Array und die Adresse an eine System.Predicate-Delegatinstanz übergeben. Die Methode Array.FindAll gibt mithilfe der von Ihnen bereitgestellten Prädikatsfunktion das entsprechende Array als ihren Rückgabewert zurück.

Sie könnten die folgende Funktion als Prädikat verwenden:

Private Function IsSmall(ByVal value As Byte) As Boolean
    Return value < 50
End Function

Dann könnten Sie einen Code wie diesen für den Abruf des Arrays verwenden:

Private Function GetSmallBytes(ByVal values() As Byte) As Byte()
    Return Array.FindAll(values, AddressOf IsSmall)
End Function

Obwohl dieses Beispiel etwas aus der Luft gegriffen ist, macht es jedoch die Einzelheiten deutlich. Wenn Sie ein spezifisches Prädikat mehrfach verwenden müssten, könnten Sie eine auf das Prädikat verweisende Variable wie folgt erstellen:

Dim pred As New System.Predicate(Of Byte)(AddressOf IsSmall)

Dann könnten Sie die Methode Array.FindAll folgendermaßen aufrufen:

Dim smallValues() As Byte = Array.FindAll(values, pred)

Obwohl die Codemenge, die man andernfalls schreiben müsste, nicht allzu groß ist, scheint es mir oft so, als ob ich Code schreibe, der ganze Sammlungen von Objekten durchläuft. Die Verwendung von Prädikaten kann Zeit sparen – sowohl beim Schreiben als auch beim Ausführen von Code.

Obwohl die Klasse System.Array all ihre Prädikat-verwandten Methoden als freigegebene Methoden zur Verfügung stellt, macht die Klasse System.Collections.Generic.List ähnliche Methoden als Instanzmethoden verfügbar. Sie könnten daher den oben dargestellten Code für ein Listenobjekt wie folgt revidieren:

Dim valueList As New List(Of Byte)
' Fill the List with random bytes, then...
Dim smallValues As List(Of Byte) = valueList.FindAll(AddressOf IsSmall)

Sowohl die Klasse System.Array als auch die Klasse System.Collections.Generic.List bietet eine Reihe von Methoden, die Prädikate nutzen können, wie in Abbildung 1 dargestellt. (Die Methoden ConvertAll und ForEach verwenden den Delegat System.Predicate in Wirklichkeit nicht. Diese Methoden ähneln allerdings den Methoden, die Prädikate verwenden, und ich habe sie daher hier mit aufgenommen. Die Konzepte sind denen, die Sie bereits gesehen haben, ähnlich, aber diese Methoden verwenden stattdessen den Delegat System.Action oder System.Converter.)

In Abbildung 2 habe ich eine einfache Beispielanwendung konstruiert, um jede dieser Methoden zu demonstrieren. In diesem Beispiel werden Array- und Listeninstanzen mit System.IO.FileInfo-Objekten gefüllt, die allen Dateien im Ordner C:\Windows entsprechen; mit dem Beispiel können Sie die verschiedenen Methoden ausprobieren, die Delegate verwenden und die Ergebnisse im Steuerelement ListBox (Listenfeld) des Formulars anzeigen. (Mit dem Beispiel für die Methode ForEach können Sie auch den Ausgabespeicherort festlegen, um den Delegat System.Action zu veranschaulichen.)

Abbildung 2: Ergebnisse von FindAll mit IsLarge
Abbildung 2:   Ergebnisse von FindAll mit IsLarge

Die Formularklasse definiert vier Variablen, die in der gesamten Anwendung verwendet werden können:

Private fileList As New List(Of FileInfo)
Private fileArray() As FileInfo

Private action As System.Action(Of FileInfo)
Private match As System.Predicate(Of FileInfo)

Die Variablen List (Liste) und Array enthalten die Dateiinformationen. Die verschiedenen Prozeduren weisen der Aktion Werte zu und vergleichen Variablen, was es unterschiedlichen Prozeduren ermöglicht, verschiedene Kriterien für übereinstimmende Dateien und für die Bearbeitung der Dateien zu verwenden. Jede dieser Variablen fungiert als eine Delegatinstanz: Der Code weist jeder Variable die Adresse einer Prozedur zu, damit die Array- und Listenmethoden, die Delegate verwenden, diese Variablen als Parameter übergeben können.

Wenn das Formular geladen wird, ruft es die Methode RefillFileInformation auf, füllt die Listen- und Arrayinstanzen mit Dateiinformationen aus und zeigt den Inhalt der Liste dann im Listenfeld des Formulars an, wie in Abbildung 3 dargestellt. Diese Prozedur zeigt mit der Methode List.ForEach Elemente im Listenfeld an:

fileList.ForEach(AddressOf DisplayFullList)

Die Prozedur DisplayFullList, die den Delegattyp System.Action aufweisen muss (das heißt eine Unterroutine, die einen einzelnen Parameter akzeptiert), fügt dem Listenfeld des Formulars jedes Element der Reihe nach hinzu:

Private Sub DisplayFullList(ByVal file As FileInfo)
    completeListBox.Items.Add( _
        String.Format("{0} ({1} bytes)", file.Name, file.Length))
End Sub

Wie Sie auf der Grundlage der Ergebnisse vermuten können, ruft die Methode List.ForEach die Methode DisplayFullList für jedes Element auf, und die Methode DisplayFullList zeigt das Element im Steuerelement ListBox (Listenfeld) an.

Das Beispielformular enthält zwei GroupBox-Steuerelemente (Gruppenfeld), mit denen Sie Delegatinstanzen für die Übereinstimmungs- und Aktionsvariablen spezifizieren können. Wenn Sie zum Beispiel auf Small Files (<500 Bytes) (Kleine Dateien (500 KB)) klicken, führt der entsprechende CheckedChanged-Ereignishandler den folgenden Code aus:

match = New System.Predicate(Of FileInfo)(AddressOf IsSmall)

Wenn Sie auf Large Files (>1 MB) (Große Dateien (1 MB)) klicken, führen Sie den folgenden Code aus:

match = New System.Predicate(Of FileInfo)(AddressOf IsLarge)

Wenn Sie auf eine der beiden Optionen im Anzeigegruppenfeld klicken, wird dieser Code ausgeführt:

action = New System.Action(Of FileInfo)(AddressOf DisplayInListBox)
' or 
action = New System.Action(Of FileInfo)(AddressOf DisplayInOutputWindow)

Die Prozedur IsSmall sieht der Prozedur System.Predicate, die Sie weiter oben gesehen haben, sehr ähnlich (die IsLarge-Prozedur ändert einfach die Größenkriterien). Der Sinn der Prozeduren IsSmall und IsLarge ist es, einfach bestimmen zu können, ob ein jeweiliges Element aus dem Array oder der Liste die von Ihnen vorgegebenen Kriterien erfüllt:

Private Function IsSmall(ByVal file As FileInfo) As Boolean
    Return file.Length < 500
End Function

Die beiden Instanzen des Delegats System.Action sehen wie der folgende Ausschnitt aus (die Beispielanwendung bestimmt anhand dieser Instanzen, was mit jedem FileInfo-Objekt geschehen soll, während der Code das Array oder die Liste durchläuft):

Private Sub DisplayInListBox(ByVal file As FileInfo)
    AddStringToListBox(String.Format("{0} ({1} bytes)", _
        file.Name, file.Length))
End Sub

Private Sub DisplayInOutputWindow(ByVal file As FileInfo)
    Debug.WriteLine(String.Format("{0} ({1} bytes)", _
        file.Name, file.Length))
End Sub

System.Predicate und System.Action

Die Methoden TrueForAll, Exists, Find, FindAll, FindLast, RemoveAll, FindIndex und FindLastIndex führen alle mit einer Instanz des Delegats System.Predicate ihre jeweiligen Aufgaben aus. Abbildung 4 zeigt Codezeilen auf, die unter Verwendung jeder dieser Methoden aus der Beispielanwendung extrahiert wurden. Abbildung 5 zeigt die Ergebnisse des Aufrufens der Methode FindAll auf, wobei das Prädikat IsLarge für die Suche von Dateien mit einer Größe von mehr als 1 MB verwendet wurde.

Abbildung 5: Verwenden von Vorhersagen mit Listen und Arrays
Abbildung 5:   Verwenden von Vorhersagen mit Listen und Arrays

Die Methode ForEach der Klassen List und Array beschreibt anhand des Delegats System.Action die Aktion, die mit jedem Element der Datenstruktur auszuführen ist. Anstatt Sie dazu zu zwingen, die Schleife zu schreiben, die jedes Element der Datenstruktur durchläuft, kann die Methode ForEach die Arbeit für Sie erledigen. Natürlich ist das Schreiben der Schleife keine beschwerliche Aufgabe, und es ist nicht wirklich der Vorteil von ForEach, dass Sie die Schleife nun nicht schreiben müssen. In meinen Tests hat die Verwendung der Methode ForEach die Leistung im Vergleich zu einem manuellen Durchlauf auch nicht erhöht. Nein, der echte Vorteil einer Verwendung von ForEach besteht darin, dass Sie die für jedes Mitglied der Datenstruktur zu verwendende Aktion ändern können, indem Sie einfach die Adresse der Prozedur ändern, die für jedes Element aufgerufen wird.

Da sich die Aktionsvariable im Beispielprojekt auf eine Instanz des Delegats System.Action bezieht, der das für jedes FileInfo-Objekt auszuführende Verhalten beschreibt, wird der folgende Code immer dann ausgeführt, wenn Sie im Formular auf die Schaltfläche ForEach klicken:

fileList.ForEach(action)
' or
Array.ForEach(fileArray, action)

Abhängig von dem Wert der Aktionsvariable zeigt der Code die Dateiinformationen entweder im Listenfeld oder im Ausgabefenster an. Eine Änderung des Verhaltens erfordert nicht, dass Sie eine Entscheidung in den Code mit einbeziehen – die Aktionsvariable definiert genau, was Sie mit jedem Element der Datenstruktur machen möchten. Das Beispielformular ermöglicht es Ihnen, entweder DisplayInListBox oder DisplayInOutputWindow als Wert für die Aktionsvariable zu wählen.

System.Converter

Vor kurzem musste ich ein Array von Ganzzahlen in ein Array von Zeichenfolgen umwandeln, damit ich die Methode String.Join auf das Array anwenden konnte. Ich habe eine gute Stunde damit verbracht, einen einfachen Weg zu finden, eine einzige Zeile Code zu schreiben, mit dem jedes Element im Array von einer Ganzzahl in eine Zeichenfolge umgewandelt wurde. Zum Schluss hatte ich den folgenden Code mit Arrays mit den Bezeichnungen „integerValue“ (Ganzzahlenwert) and „stringValues“ (Zeichenfolgenwerte):

Dim stringValues(integerValues.Length – 1) As String
For i As Integer = 0 To integerValues.Length - 1
    stringValues(i) = integerValues(i).ToString
Next
Return String.Join(", ", stringValues)

Was ich wirklich wollte, war eine einzige Prozedur, die ich aufrufen konnte und die diese Aufgabe automatisch erledigen sollte. Leider habe ich die genau passende Prozedur übersehen: die Methode Array.ConvertAll. Bei dieser Methode stellen Sie eine System.Converter-Delegatinstanz bereit, die die Konvertierung für jedes einzelnen Element durchführt und dann die Methode ConvertAll aufruft, um die Arbeit zu erledigen. Sie müssen kein Ausgabearray erstellen oder ein solches mit den Werten ausfüllen.

Für das Beispiel oben hätte ich eine Umwandlungsprozedur wie diese erstellen können:

Private Function MyConverter(ByVal value As Integer) As String
    Return value.ToString()
End Function

Zum Aufrufen der Prozedur hätte ich den folgenden Code schreiben können:

stringValues = Array.ConvertAll(Of Integer, String)(
    integerValues, AddressOf MyConverter)

Das Aufrufen der Methode ConvertAll erfordert etwas Vorsicht: Sie müssen die Ein- und Ausgabetypen zusammen mit dem Array und der Adresse der System.Converter-Delegatinstanz bereitstellen.

Da die Listenklasse die Methode ConvertAll als eine Instanzmethode bereitstellt, müssen Sie nur den Ausgabetyp angeben. Daher ist es etwas einfacher, die Methode ConvertAll einer Listeninstanz aufzurufen, was dann wie folgt aussehen könnte:

stringValues = integerValues.ConvertAll(Of String)(
    AddressOf MyConverter)

Im Beispielformular ist ein ähnliches Beispiel enthalten, bei dem ein FileInfo-Objekt in eine Zeichenfolge umgewandelt wird (indem die Eigenschaft FullName des FileInfo-Objekts zurückgegeben wird). Im Beispiel wird der folgende Konverter verwendet:

Private Function FileInfoToString(ByVal file As FileInfo) As String
    Return file.FullName
End Function

Wenn Sie auf die Schaltfläche ConvertAll des Beispielformulars klicken, wird der folgende Code ausgeführt:

Dim fileNames As List(Of String) = 
    fileList.ConvertAll(Of String)(AddressOf FileInfoToString)

Es erscheint vielleicht etwas seltsam, dass Sie den generischen Ausgabetyp angeben müssen, wenn Sie die Prozedur ConvertAll aufrufen. Es wäre zu erwarten, dass Sie die Prozedur wie folgt aufrufen könnten:

' This code won't compile:
fileNames = fileList.ConvertAll(AddressOf FileInfoToString)

Da der Compiler den Typ der Ausgabe für die Konvertierung kennen muss, müssen Sie diese Informationen zu dem Zeitpunkt angeben, zu dem Sie den Code schreiben. Da es sich um eine freigegebene Methode handelt und der Compiler daher weder Informationen über den Eingabe- noch den Ausgabetyp hat, ist es erforderlich, dass Sie sowohl Eingabe- als auch Ausgabetyp angeben, um die Methode Array.ConvertAll aufzurufen:

fileNames = Array.ConvertAll(Of FileInfo, String)(
    fileArray, AddressOf FileInfoToString)

Sie müssen diese Methoden vielleicht einige Male ausprobieren, bevor Sie sie vollständig aufgenommen haben, aber wenn Sie das Konzept erst einmal richtig verstanden haben, kann Ihnen die Methode ConvertAll beim Erstellen des Codes und zur Laufzeit Zeit sparen.

Weiterführende Informationen

Sie haben vielleicht schon vermutet, dass sich Generika in viele Bereiche von .NET Framework 2.0 eingeschlichen haben. Wenn Sie sich dieses wichtige neue Feature noch nicht genauer angesehen haben, nehmen Sie sich etwas Zeit, um mehr darüber herauszufinden. Ich habe Generika im Artikel vom September 2005 vorgestellt, und auf der MSDN®-Website finden Sie viele weiterführende Informationen über die Verwendung und das Erstellen generischer Prozeduren. Wenn Sie in .NET Framework auf eine Methode stoßen, für die Sie wie in den hier aufgeführten Beispielen eine Instanz eines Generikums bereitstellen müssen, laufen Sie nicht weg – versuchen Sie einfach, die Details auszuarbeiten. Sie können viel Zeit und Mühe sparen, indem Sie die Vorteile von Generika in Ihren Anwendungen nutzen.

Senden Sie Fragen und Kommentare in englischer Sprache an basics@microsoft.com.

Der Autor

Ken Getz ist leitender Berater bei MCW Technologies und ein Autor von Lehrgangsmaterial für AppDev (http://www.appdev.com/). Er ist Mitverfasser von ASP .NET Developers Jumpstart (Addison-Wesley, 2002), Access Developer's Handbook (Sybex, 2002) und VBA Developer's Handbook, 2nd Edition (Sybex, 2001). Sie erreichen ihn unter keng@mcwtech.com. Ken möchte Russ Nemhauser für die Bereitstellung der amüsanten IM und für die Inspiration für diesen Artikel danken.

Aus der Ausgabe vom September 2006 des MSDN-Magazins.

Anzeigen:
© 2014 Microsoft