CLR

Neues aus der .NET 4.5-Basisklassenbibliothek

Immo Landwerth

 

In der Microsoft .NET Framework-Basisklassenbibliothek (BCL) geht es um Grundlagen. Obwohl einige der Basiskonstrukte stabil sind und sich nicht stark verändern (zum Beispiel System.Int32 und System.String), investiert Microsoft in diesen Bereich noch viel Entwicklungsarbeit. Dieser Artikel behandelt die größeren (sowie auch einige der kleineren) Verbesserungen, die das NET Framework 4.5 der BCL erhalten hat.

Bitte beachten Sie, dass dieser Artikel auf der Betaversion des .NET Framework 4.5 basiert und nicht auf dem Endprodukt oder den APIs, sodass diese Features durchaus noch geändert werden können.

Einen Überblick über die anderen Bereiche des .NET Framework, wie die Windows Communication Foundation (WCF) oder die Windows Presentation Foundation (WPF), finden Sie auf den Seiten der MSDN-Bibliothek unter "Neuerungen in .NET Framework 4.5 Beta" (bit.ly/p6We9u).

Vereinfachte asynchrone Programmierung

Die Verwendung einer asynchronen E/A-Schnittstelle hat mehrere Vorteile: Sie hilft dabei, ein Blockieren der Benutzungsoberfläche zu verhindern, und reduziert außerdem die Anzahl der vom Betriebssystem verwendeten Threads. Und dennoch besteht die Möglichkeit, dass Sie diese Vorteile womöglich gar nicht nutzen, denn die asynchrone Programmierung galt lange Zeit als sehr komplex. Das größte Problem bestand darin, dass das vorausgehende asynchrone Programmiermodell (APM) nur auf Begin-/End-Methoden ausgelegt war. Zur Veranschaulichung dieses Modells soll zunächst eine direkte synchrone Methode betrachtet werden, die einen Stream kopiert:

public void CopyTo(Stream source, Stream destination)
{
  byte[] buffer = new byte[0x1000];
  int numRead;
  while ((numRead = source.Read(buffer, 0, buffer.Length)) != 0)
  {
    destination.Write(buffer, 0, numRead);
  }
}

In Abbildung 1 ist der Quellcode dargestellt, der diese Methode in das frühere asynchrone Programmiermodell abbildet.

Abbildung 1 Asynchrones Kopieren eines Streams auf die herkömmliche Art

public void CopyToAsyncTheHardWay(Stream source, Stream destination)
{
  byte[] buffer = new byte[0x1000];
  Action<IAsyncResult> readWriteLoop = null;
  readWriteLoop = iar =>
  {
    for (bool isRead = (iar == null); ; isRead = !isRead)
    {
      switch (isRead)
      {
        case true:
          iar = source.BeginRead(buffer, 0, buffer.Length, 
            readResult =>
          {
            if (readResult.CompletedSynchronously) return;
            readWriteLoop(readResult);
          }, null);
          if (!iar.CompletedSynchronously) return;
          break;
        case false:
          int numRead = source.EndRead(iar);
          if (numRead == 0)
          {
            return;
          }
          iar = destination.BeginWrite(buffer, 0, numRead, 
            writeResult =>
          {
            if (writeResult.CompletedSynchronously) return;
            destination.EndWrite(writeResult);
            readWriteLoop(null);
          }, null);
          if (!iar.CompletedSynchronously) return;
          destination.EndWrite(iar);
          break;
        }
      }
  };
  readWriteLoop(null);
}

Sicherlich ist die asynchrone Version nicht so einfach zu verstehen wie das synchrone Gegenstück. Die Absicht ist aus dem Standardcode nur schwer zu erkennen. Solche Standardbausteine sind jedoch für die Umsetzung grundlegender Programmiersprachenkonstrukte (wie z. B. Schleifen) erforderlich, wenn Delegate darin enthalten sind. Wenn dieses Beispiel noch nicht abschreckend genug ist, versuchen Sie einmal, eine Ausnahmebedingung oder einen Abbruch auf diese Weise darzustellen.

Glücklicherweise bietet die aktuelle Version der Basisklassenbibliothek nun ein neues asynchrones Programmiermodell, das Task und Task <T> verwendet. C# und Visual Basic haben durch Hinzufügen der Schlüsselwörter async und await eine ausgezeichnete Sprachunterstützung entwickelt (für F# gab es übrigens bereits eine Sprachunterstützung, die auf asynchrone Workflows zurückgriff und damit tatsächlich als Inspirationsquelle für dieses Feature diente). Als Folge dessen können Compiler nun den Standardcode zu einem Großteil (wenn auch nicht vollständig) verarbeiten, den man früher schreiben musste. Die neue Sprachunterstützung sowie auch manche API-Zusätze zum .NET Framework sind aufeinander abgestimmt, sodass asynchrone Methoden genau so einfach geschrieben werden können wie synchroner Programmiercode. Sehen Sie selbst – um die CopyTo-Methode asynchron darstellen zu können, sind lediglich die hervorgehobenen Einschübe erforderlich:

public async Task CopyToAsync(Stream source, Stream destination)
{
  byte[] buffer = new byte[0x1000];
  int numRead;
  while ((numRead = await 
     source.ReadAsync(buffer, 0, buffer.Length)) != 0)
  {
    await destination.WriteAsync(buffer, 0, numRead);
  }
}

Es gibt viele interessante Aspekte, die die asynchrone Programmierung mithilfe der neuen Sprachfeatures hervorgebracht hat; dennoch soll im Weiteren der Fokus auf die Folgen gerichtet werden, die sich daraus für die Basisklassenbibliothek BCL und für Sie als Benutzer ergeben. Der interessierte Leser sei an dieser Stelle auf den Abschnitt "Asynchrone Programmierung mit Async und Await (in C# und Visual Basic)" der MSDN-Bibliothek verwiesen (bit.ly/nXerAc).

Um eigene asynchrone Operationen zu erzeugen, sind Low-Level-Komponenten erforderlich. Darauf aufbauend kann man komplexere Methoden entwickeln, wie die zuvor beschriebene CopyToAsync-Methode. Diese Methode benötigt nur die ReadAsync- und WriteAsync-Methoden der Stream-Klasse als Bausteine. Abbildung 2 listet die wichtigsten asynchronen APIs auf, die zur BCL hinzugefügt wurden.

Abbildung 2 Asynchrone Methoden der BCL

Typ Methoden
System.IO.Stream

ReadAsync

WriteAsync

FlushAsync

CopyToAsync

System.IO.TextReader

ReadAsync

ReadBlockAsync

ReadLineAsync

ReadToEndAsync

System.IO.TextWriter

WriteAsync

WriteLineAsync

FlushAsync

Beachten Sie hierbei, dass wir keine asynchronen Versionen für APIs mit einer hohen Granularität hinzugefügt haben, wie z. B. TextReader.Peek. Grund dafür ist, dass auch asynchrone APIs einen gewissen zusätzlichen Aufwand erfordern und wir die Entwickler dadurch nicht auf eine falsche Fährte lotsen wollen. Aus diesem Grund haben wir uns insbesondere auch dagegen entschieden, asynchrone Versionen für BinaryReader- oder BinaryWriter-Methoden bereitzustellen. Um solche APIs verwenden zu können, empfehlen wir, in der Anwendung Task.Run einen neuen asynchronen Prozess zu starten und anschließend die synchronen APIs innerhalb dieses Prozesses aufzurufen. Dies sollte jedoch nicht durch einen Methodenaufruf erfolgen. Die grundsätzliche Empfehlung lautet: Ein asynchroner Prozess sollte so grob wie möglich sein. Wenn Sie z. B. von einem Stream 1.000 Int32s mit dem BinaryReader auslesen möchten, ist es besser, eine einzige Task zu starten, um zeitgleich alle 1.000 Int32s auf einmal zu verarbeiten, als 1.000 einzelne Tasks zu starten, die jeweils nur ein einziges Int32 auslesen.

Wer mehr über asynchrone Programmierung erfahren möchte, sei auf den Blog "Parallel Programming with .NET" (Paralleles Programmieren mit .NET) verwiesen (blogs.msdn.com/b/pfxteam).

Schreibgeschützte Sammelschnittstellen

Eine der schon lange ausstehenden Funktionsanforderungen an die BCL war die einer schreibgeschützten Sammelschnittstelle (nicht zu verwechseln mit unveränderlichen Schnittstellen; siehe dazu Seite 22: "Verschiedene Konzepte schreibgeschützter Sammlungen"). Wir waren der Meinung, dass diese besondere Anforderung (Schreibschutz) am besten durch eine optionale Zusatzfunktion abgebildet werden kann. In diesem Entwurfsmuster wird eine API zur Verfügung gestellt, mit deren Hilfe der Benutzer testet, ob eine gegebene Funktion nicht unterstützt wird, und dann eine NotSupportedException auslösen kann.

Der Vorteil dieses Entwurfsmusters ist, dass es weniger Typen erfordert, da keine Kombinationen von Funktionen modelliert werden müssen. Die Stream-Klasse z. B. enthält verschiedene Funktionen, die alle mit Booleschen get-Accessors ausgedrückt werden (CanSeek, CanRead, CanWrite und CanTimeout). Auf diese Weise ermöglicht die BCL die Verwendung eines einzigen Typs für Streams und unterstützt dennoch jede Kombination von Streamingfunktionen.

Mit der Zeit wurde uns klar, dass man für die Verfügbarkeit einer schreibgeschützten Sammelschnittstelle die zusätzlich entstandene Komplexität in Kauf nehmen sollte. Als Erstes möchte ich Ihnen die Schnittstellen vorstellen und anschließend die angebotenen Funktionen erläutern. Abbildung 3 zeigt ein Klassendiagramm aus Visual Studio der vorhandenen (veränderbaren) Sammelschnittstellen; Abbildung 4 zeigt die entsprechenden schreibgeschützten Schnittstellen.

Mutable Collection Interfaces
Abbildung 3 Veränderbare Sammelschnittstellen

Read-Only Collection Interfaces
Abbildung 4: Schreibgeschützte Sammelschnittstellen

Falls Sie die IReadOnlyCollection<T> vermissen: Diese wurde nicht in die Betaversion des .NET Framework 4.5 aufgenommen. In der Betaversion werden IReadOnlyList<T> und IReadOnlyDictionary<TKey,TValue> direkt von IEnumerable<T> abgeleitet; jede Schnittstelle definiert ihre eigene Count-Eigenschaft.

IEnumerable<T> ist kovariant. Das heißt, wenn eine Methode eine IEnumerable<Shape> akzeptiert, kann diese mit der IEnumerable<Circle> aufgerufen werden (unter der Voraussetzung, dass Circle eine Ableitung von Shape ist). Dies ist nützlich in Szenarien mit Typenhierarchien und Algorithmen, die mit spezifischen Typen operieren, wie z. B. eine Anwendung, die verschiedene Formen zeichnen kann. IEnumerable<T> ist für die meisten Szenarien ausreichend, in denen Typensammlungen verarbeitet werden. Manchmal jedoch ist mehr Rechenleistung erforderlich als dieser Vorgang vorsieht:

  1. Materialization.IEnumerable<T> kann nicht ausdrücken, ob die Sammlung bereits verfügbar ("materialisiert") ist oder ob sie bei jedem Durchlauf immer wieder neu berechnet wird (z. B. wenn sie für eine LINQ-Abfrage steht). Wenn ein Algorithmus eine Sammlung mehrere Male durchlaufen muss und die Berechnung der Sequenz aufwendig ist, kann dies Rechenleistung kosten; wenn in aufeinander folgenden Durchläufen die Objekte mehrfach generiert werden, können aufgrund von Verwechslungen kleinere Fehler auftreten.
  2. Count.IEnumerable<T> liefert keine Zahl. Tatsächlich muss darin noch nicht einmal eine Zahl enthalten sein, denn es kann sich um eine infinite Sequenz handeln. In vielen Fällen ist jedoch die statische Erweiterungsmethode Enumerable.Count ausreichend. Erstens spezifiziert sie bekannte Sammlungstypen wie ICollection<T>, sodass ein wiederholter Durchlauf der kompletten Sequenz vermieden wird; zweitens ist die Berechnung des Ergebnisses in vielen Fällen nicht aufwendig. In Abhängigkeit von der Größe der Sammlung kann dies jedoch einen Unterschied ausmachen.
  3. Indexing.IEnumerable<T> gestattet keinen zufälligen Zugriff auf die Elemente. Manche Algorithmen, wie der Quick Sort, können ein Element nur anhand seines Indexes finden. Auch hierfür gibt es wieder eine statische Extensionsmethode (Enumerable.ElementAt), die einen optimierten Codepfad verwendet, wenn die gegebene Enumerable durch eine IList<T>-Methode gesichert wird. Wenn die Indizierung jedoch in einer Schleife verwendet wird, kann ein linearer Scan fatale Auswirkungen auf die Leistung haben und den schönen O(n) Algorithmus in einen O(n2) Algorithmus verwandeln. Wenn Sie also zufälligen Zugriff haben möchten, dann bedeutet dies wirklich "zufälliger" Zugriff.

Warum werden also nicht einfach ICollection<T>/IList<T> anstelle der IEnumerable<T> verwendet? Weil dadurch Kovarianz verloren geht und man nicht mehr Methoden, die nur die Auflistungen lesen, von solchen, die auch Auflistungen verändern, unterscheiden kann. Diese Unterscheidbarkeit wird jedoch im Bereich asynchroner Programmierung oder Multithreading im Allgemeinen zunehmend wichtiger. Mit anderen Worten: Sie möchten Ihren eigenen Kuchen auch selbst essen.

Aufrufen von IReadOnlyCollection<T> und IReadOnlyList<T>. IReadOnlyCollection<T> ist im Grunde dasselbe wie IEnumerable<T>, aber die Methode fügt noch eine Count-Eigenschaft hinzu. Dadurch können Algorithmen erzeugt werden, die ausdrücken, dass materialisierte Auflistungen oder Auflistungen mit einer bekannten und begrenzten Größe erforderlich sind. IReadOnlyList<T> bietet eine Erweiterung durch Hinzufügen eines Indizierers. Diese beiden Schnittstellen sind kovariant, d. h., wenn eine Methode eine IReadOnlyList<Shape> akzeptiert, kann diese mit der IEnumerable<Circle> aufgerufen werden:

class Shape { /*...*/ }
class Circle : Shape { /*...*/ }
void LayoutShapes(IReadOnlyList<Shape> shapes) { /*...*/ }
void LayoutCircles()
{
  List<Circle> circles = GetCircles();
  LayoutShapes(circles);
}

Leider ist es mit unserem Typsystem nicht möglich, T-Typen kovariant zu machen, es sei denn, es gibt keine Methoden, die T als Eingabe verwenden. Daher können wir keine IndexOf-Methode zu IReadOnlyList<T> hinzufügen. Unserer Meinung nach ist dieses Opfer jedoch im Vergleich zu dem Fall, dass Kovarianz gar nicht unterstützt würde, gering.

Sämtliche unserer integrierten Sammlungsimplementierungen, wie Arrays, List<T>, Collection<T> und ReadOnlyCollection<T>, implementieren auch schreibgeschützte Sammelschnittstellen. Da nun jede Sammlung als schreibgeschützte Sammlung behandelt werden kann, können Algorithmen nun viel präziser eine Absicht deklarieren, ohne dass der Grad der Wiederverwendung begrenzt werden muss – sie können für sämtliche Sammlungstypen verwendet werden. Im vorausgehenden Beispiel kann der Benutzer von LayoutShapes eine List<Circle>-Methode übergeben, aber LayoutShapes würde auch einen Circle-Array oder eine Collection<Circle>-Methode akzeptieren.

Ein anderer Nutzen dieser Sammlungstypen ist die große Benutzerfreundlichkeit, die sie für die Arbeit mit Windows Runtime (WinRT) bieten. WinRT stellt eigene Auflistungstypen zur Verfügung, wie Windows.Foundation.Collections.IIterable<T> und Windows.Foundation.Collec­tions.IVector<T>. Die CLR-Metadatenebene verbindet diese direkt mit den entsprechenden BCL-Datentypen. Wenn z. B. WinRT aus .NET Framework verwendet wird, wird IIterable<T> zu IEnumerable<T> und IVector<T> wird zu IList<T>. Tatsächlich ist sich ein Entwickler, der Visual Studio und IntelliSense verwendet, gar nicht bewusst, dass es in WinRT unterschiedliche Sammlungstypen gibt. Da WinRT ebenfalls Versionen mit einem Nur-Lesezugriff verwendet (wie IVectorView<T>), ergänzen die neuen Schnittstellen mit Nur-Lesezugriff das Gesamtwerk, sodass alle Sammlungstypen leicht zwischen dem .NET Framework und WinRT austauschbar sind.

Verschiedene Konzepte schreibgeschützter Sammlungen

  • Veränderbar (bzw. nicht schreibgeschützt) Der häufigste Sammlungstyp in der .NET Welt. Hierbei handelt es sich um Sammlungen wie List<T>, die das Lesen, Hinzufügen, Entfernen und Verändern von Elementen gestatten.
  • Schreibgeschützt Diese Sammlungen können von außen nicht verändert werden. Dennoch garantiert diese Art Sammlung es nicht, dass ihre Inhalte nie verändert werden. Zum Beispiel können die Schlüssel- und Wertesammlungen in einem Wörterbuch nicht direkt aktualisiert werden, aber durch indirektes Hinzufügen werden die Schlüssel- und Wertesammlungen im Wörterbuch aktualisiert.
  • Unveränderlich Diese Sammlungen werden einmal erzeugt und garantiert nie verändert. Dies ist eine nützliche Eigenschaft für Multithreading. Wenn eine komplizierte Datenstruktur komplett unveränderbar ist, kann sie immer sicher an einen Entwickler übergeben werden, der im Hintergrund arbeitet. Es besteht keine Gefahr, dass jemand etwas zeitgleich verändern kann. Dieser Auflistungstyp wird zurzeit nicht von der Microsoft .NET Framework-Basisklassenbibliothek bereitgestellt.
  • Freezable Diese Sammlungen verhalten sich solange wie veränderbare Sammlungen, bis sie "eingefroren" werden. Danach verhalten sie sich wie unveränderbare Sammlungen. Obwohl die BCL diese Sammlungen nicht definiert, kann man sie in der Windows Presentation Foundation nachschlagen.

Andrew Arnott hat einen ausgezeichneten Blogbeitrag verfasst, der die verschiedenen Arten von Sammlungen im Detail beschreibt (bit.ly/pDNNdM).

Unterstützung für ZIP-Archive

Eine weitere Anforderung, die mit der Zeit notwendig wurde, war die Unterstützung für das Lesen und Schreiben regulärer ZIP-Archive. Ab Version .NET Framework 3.0 wird das Lesen und Schreiben von Archiven in der Open Packaging Convention unterstützt (bit.ly/ddsfZ7). Jedoch war System.IO.Packaging auf diese besondere Spezifikation zugeschnitten und kann im Allgemeinen nicht für die Verarbeitung gewöhnlicher ZIP-Archive verwendet werden.

Diese Version bietet mithilfe von System.IO.Com­pression.ZipArchive eine hervorragende ZIP-Unterstützung. Zusätzlich haben wir einige schon lange bemängelten Schwierigkeiten bezüglich Leistung und Kompressionsqualität unserer DeflateStream-Implementierung behoben. Ab .NET Framework 4.5 verwendet die Deflate­Stream-Klasse die bekannte zlib-Bibliothek. Dadurch stellt sie eine bessere Implementierung des deflate-Algorithmus zur Verfügung und erzeugt in den meisten Fällen auch eine stärker komprimierte Datei, als es in den vorherigen Versionen des .NET Framework möglich war.

Für das Extrahieren eines kompletten Archivs auf der Festplatte ist nur eine einzige Zeile Quellcode erforderlich:

ZipFile.ExtractToDirectory(@"P:\files.zip", @"P:\files");

Es ist ebenfalls sichergestellt, dass die typischen Operationen nicht das Einlesen des kompletten Archivs in den Speicher erfordern. Das Extrahieren einer einzigen Datei aus einem großen Archiv kann z. B. wie folgt aussehen:

using (ZipArchive zipArchive = 
  ZipFile.Open(@"P:\files.zip", ZipArchiveMode.Read))
{
  foreach (ZipArchiveEntry entry in zipArchive.Entries)
  {
    if (entry.Name == "file.txt")
    {
      using (Stream stream = entry.Open())
      {
        ProcessFile(stream);
      }
    }
  }     
}

In diesem Fall wird nur das Inhaltsverzeichnis des ZIP-Archivs in den Speicher geladen. Die extrahierte Datei wird vollständig gestreamt; dies bedeutet, dass sie nicht zwangsläufig in den Speicher passen muss. Auf diese Weise können beliebig große ZIP-Archive verarbeitet werden, auch wenn der Speicherplatz begrenzt ist.

Das Erzeugen von ZIP-Archiven funktioniert auf ähnliche Weise. Um ein ZIP-Archiv aus einem Verzeichnis zu erstellen, ist auch wiederum nur eine einzige Zeile Quellcode erforderlich:

ZipFile.CreateFromDirectory(@"P:\files", @"P:\files.zip");

Natürlich kann ein ZIP-Archiv auch manuell erstellt werden, wodurch die vollständige Kontrolle über die interne Struktur erhalten bleibt. Das folgende Code-Beispiel zeigt, wie ein ZIP-Archiv erzeugt werden kann, zu dem nur C#-Quellcodedateien hinzugefügt werden (zusätzlich erhält dieses ZIP-Archiv nun ein Unterverzeichnis mit dem Namen SourceCode):

IEnumerable<string> files = 
  Directory.EnumerateFiles(@"P:\files", "*.cs");
using (ZipArchive zipArchive = 
  ZipFile.Open(@"P:\files.zip", ZipArchiveMode.Create))
{
  foreach (string file in files)
  {
    var entryName = Path.Combine("SourceCode", Path.GetFileName(file));
    zipArchive.CreateEntryFromFile(file, entryName);
  }
}

Mithilfe von Streams können ebenfalls ZIP-Archive erzeugt werden, deren Inhalt nicht als eine tatsächliche Datei vorliegt. Das gilt auch für das ZIP-Archiv selbst. Anstelle der Funktion ZipFile.Open kann auch der ZipArchive-Konstruktor verwendt werden, der einen Stream verwendet. Auf diese Weise kann man z. B. auch einen Webserver entwickeln, der aus den Inhalten, die in einer Datenbank abgespeichert werden, dynamisch ZIP-Archive erzeugt und die Ergebnisse nicht auf die Festplatte, sondern ebenfalls direkt in den Antwort-Stream schreibt.

Ein Aspekt bezüglich der API-Gestaltung sollte etwas genauer erläutert werden. Ihnen ist wahrscheinlich aufgefallen, dass die statischen Hilfsmethoden in der Klasse ZipFile definiert werden, wohingegen die aktuelle Instanz die Typenbezeichnung ZipArchive trägt. Woran liegt das?

Ab .NET Framework 4.5 wurden die APIs für Portabilität konzipiert. Manche .NET-Plattformen unterstützen keine Datenpfade, wie z. B. .NET für Metro-style Apps auf Windows 8. Auf dieser Plattform ist der Zugang zum Dateisystem als eine Vermittlungsstelle zu einem funktionsbasierten Modell vorgesehen. Für das Lesen oder Schreiben von Dateien können keine regulären Win32-APIs mehr verwendet werden. Stattdessen müssen WinRT-APIs verwendet werden.

Wie bereits zuvor gezeigt, sind für eine ZIP-Funktionalität überhaupt keine Dateipfade erforderlich. Daher lässt sich die Funktionalität in zwei Bereiche aufteilen:

  1. System.IO.Compression.dll. Diese Assembly enthält eine allgemeine ZIP-Funktionalität. Sie unterstützt keine Dateipfade. Die Hauptklasse dieser Assembly ist ZipArchive.
  2. System.IO.Compression.FileSystem.dll. Diese Assembly stellt eine statische ZipFile-Klasse zur Verfügung, in der Erweiterungsmethoden und statische Hilfefunktionen definiert sind. Solche APIs erweitern die ZIP-Funktionalität um Hilfsmethoden für die .NET-Plattformen, die Systemdateipfade unterstützen.

Wer mehr über das Programmieren von portablem .NET-Code erfahren möchte, sei auf den Abschnitt "Portable Klassenbibliotheken" in der MSDN-Bibliothek verwiesen: bit.ly/z2r3eM.

Verschiedene Verbesserungen

Natürlich gibt es noch eine Reihe weiterer Neuerungen. Nach einer so langen Arbeit an einer Version wirkt das Aufzählen der wichtigsten Funktionen beinahe so, als würde man sein Lieblingskind beim Namen nennen. Abschließend sollen noch einige nennenswerte Bereiche kurz aufgegriffen werden, die im Artikel nicht ihren verdienten Platz erhalten konnten.

AssemblyMetadataAttribute Wenn wir aus dem .NET Framework etwas über Attribute gelernt haben, dann ist es Folgendes: Egal wie viele man bereits hat, man braucht immer noch mehr. (Übrigens verfügte .NET Framework 4 bereits über mehr als 300 Attribute auf Assemblyebene). AssemblyMetadataAttribute ist ein allgemein nützliches Assemblyattribut, durch das ein stringbasiertes Schlüsselwertpaar mit einer Assembly assoziiert werden kann. Dies kann dazu verwendet werden, um auf die Homepage des Produkts zu verweisen oder auf das Versionskontrolllabel, das der Quelle entspricht, aus der die Assembly aufgebaut wurde.

WeakReference<T> Beim vorhandenen nicht generischen WeakReferenz-Typ gibt es zwei Probleme in der Anwendung: Erstens zwingt er den Benutzer immer dann zu einer Umwandlung, wenn der Zugriff auf das Ziel erfolgen muss. Zweitens jedoch, und das ist noch wichtiger, besitzt er einen Designfehler, der ihn für Racebedingungen anfällig macht: Er macht eine API verfügbar, um zu kontrollieren, ob das Objekt noch vorhanden ist (IsAlive), und eine weitere, um das tatsächliche Objekt (Target) zu erreichen. WeakReference<T> behebt das Problem durch Bereitstellung einer einzigen TryGetTarget-API, die beide Operationen auf atomare Weise ausführt.

Comparer<T>.Create(Comparison<T>) Die BCL bietet zwei Möglichkeiten, um Comparer von Sammlungen zu implementieren. Eine Möglichkeit ist die Schnittstelle IComparer<T>; die andere nutzt den Delegat Comparison<T>. Die Umwandlung eines IComparer<T> zu Comparison<T> ist einfach. Die meisten Sprachen ermöglichen es, eine Methode in einen Delegattyp mittels impliziter Konversion zu verwandeln, also lässt sich leicht die Comparison<T>-Methode von der IComparer<T> Compare-Methode ableiten. In der anderen Richtung musste jedoch eine eigene IComparer<T>-Methode implementiert werden. Wir haben im .NET Framework 4.5 eine statische Create-Methode für den Comparer <T> hinzugefügt, die für einen gegebenen Comparison<T> eine Implementierung für den IComparer<T> liefern.

ArraySegment<T> Im .NET Framework 2.0 haben wir die Struktur ArraySegment<T> hinzugefügt, durch die ein Unterbereich eines gegebenen Arrays dargestellt werden kann, ohne dass ein Kopieren erforderlich ist. Leider konnte ArraySegment<T> keine der Sammelschnittstellen implementieren. Dies hinderte Sie daran, diese an Methoden zu übergeben, die auf den Sammelschnittstellen ausgeführt wurden. In dieser Version wurde der Fehler behoben. ArraySegment<T> implementiert nun IEnumerable, IEnumerable<T>, ICollection<T> und IList<T> sowie IReadOnlyCollection<T> und IReadOnlyList<T>.

SemaphoreSlim.WaitAsync Dies ist der einzige primitive Synchronisierungstyp, der das Warten auf die Sperre unterstützt. Weitere Informationen über die Rationale erfahren finden Sie unter "What’s New for Parallelism in .NET 4.5 Beta" (Neuerungen für den Parallelismus in .NET 4.5 Beta) (bit.ly/AmAUIF).

ReadOnlyDictionary<TKey,TValue> Ab .NET Framework 2.0 stellt die BCL den ReadOnlyCollection<T> zur Verfügung, der als ein Wrapper mit Schreibschutz eine gegebene Sammlungsinstanz umfasst. Dies ermöglicht es den Entwicklern von Objektmodellen, Sammlungen öffentlich zu machen, die der Benutzer nicht verändern kann. In dieser Version haben wir das gleiche Konzept für Wörterbücher hinzugefügt.

BinaryReader, BinaryWriter, StreamReader, StreamWriter: Hierbei handelt es sich um eine Option, mit der ein zugrunde liegender Stream verwaltet werden kann. Die Lese- und Schreibklassen der höheren Ebenen akzeptieren alle eine Stream-Instanz in ihren Konstruktoren. In den vorausgehenden Releases bedeutete dies, dass der Besitz des Streams auf die Lese-/Schreib-Instanz übertragen wurde, was ebenfalls bedeutete, dass der Leser/Schreiber auch den zugrunde liegenden Stream verwaltete. Dies ist sicherlich schön und praktisch, wenn es nur einen einzigen Leser/Schreiber gibt, aber umständlicher in solchen Fällen, in denen man mehrere unterschiedliche APIs zusammensetzen muss, die alle Streams ausführen, aber Leser/Schreiber als Teil ihrer Implementierung verwenden. In den vorherigen Versionen bestand die Lösung darin, den Leser/Schreiber nicht zu verwalten und einen Kommentar in der Quelle zu hinterlassen, der dieses Problem erläuterte (dieses Vorgehen erforderte ebenfalls, dass der Schreiber manuell abgearbeitet werden musste, um Datenverluste zu vermeiden). Mit .NET Framework 4.5 ist es möglich, diesen "Vertrag" durch einen Leser/Schreiber-Konstruktor auszudrücken, der den Parameter leaveOpen verwendet; in diesem kann explizit spezifiziert werden, dass der Leser/Schreiber nicht den zugrunde liegenden Stream verwaltet.

Console.IsInputRedirected-, Console.IsOutputRedirected- und Console.IsErrorRedirected- Kommandozeilenprogramme unterstützen das Umleiten von Ein- und Ausgabe. Für die meisten Anwendungen ist dies transparent. Manchmal jedoch ist ein anderes Verhalten erwünscht, wenn eine Umleitung aktiv ist. Zum Beispiel ist eine farbige Schrift als Konsolenausgabe ebenso wenig hilfreich wie das Festlegen einer Cursorposition. Diese Eigenschaften gestatten eine Abfrage, ob ein Standardstream umgeleitet wurde.

Console.OutputEncoding und Console.InputEncoding können nun für Encoding.Unicode verwendet werden. Das Festlegen von Console.OutputEncoding als Encoding.Unicode gestattet dem Programm, auch Zeichen zu verwenden, die nicht auf der mit der Konsole verbundenen OEM-Codeseite dargestellt werden konnten. Dieses Feature erleichtert es ebenfalls, den Text in verschiedenen Skripten auf der Konsole darzustellen. Weitere Details werden in der nächsten überarbeiteten Dokumentation in der MSDN-Bibliothek für die Klasse Console verfügbar sein.

ExceptionDispatchInfo Die Fehlerbehandlung ist ein wichtiger Aspekt beim Konstruieren von Framework-Komponenten. Manchmal reicht es nicht aus, eine Ausnahme erneut auszulösen (in C# durch "throw"), da dies nur innerhalb eines Ausnahmehandlers passieren kann. Manche Frameworkkomponenten, wie die Task-Infrastruktur, müssen die Ausnahme zu einem späteren Punkt auslösen (z. B. nachdem sie zum ursprünglichen Thread geleitet wurde). In der Vergangenheit bedeutete dies, dass der Originalpfad und die Windows Error Reporting (WER)-Klassifikation (auch bekannt als Watson Bucket) verloren war, da das erneute Auslösen desselben Ausnahmeobjekts diese Information einfach überschreiben würde. ExceptionDispatchInfo gestattet es, ein vorhandenes Ausnahmeobjekt zu erfassen und erneut auszulösen, ohne irgendwelche der gültigen Informationen zu verlieren, die in dem Ausnahmeobjekt gespeichert waren.

Regex.Timeout Reguläre Ausdrücke eignen sich hervorragend zum Überprüfen der Eingabe. Es ist jedoch nicht weit bekannt, dass bestimmte reguläre Ausdrücke sehr aufwändig zu berechnen sind, wenn sie auf spezifische Texteingaben angewendet werden; d. h., ihre Komplexität erfordert einen exponentiellen Aufwand an Zeit. Besonders problematisch ist dies in Serverumgebungen, in denen tatsächliche reguläre Ausdrücke Teil der Konfiguration sind. Da es schwierig ist, wenn nicht sogar unmöglich, das Laufzeitverhalten eines gegebenen regulären Ausdrucks vorherzusagen, ist es in solchen Fällen am sichersten, die Anzahl der Versuche einzuschränken, die das Regex-Modul zur Verarbeitung des gegebenen Input unternehmen soll. Daher nutzt Regex nun verschiedene APIs, die einen Timeout akzeptieren: Regex.IsMatch, Regex.Match, Regex.Matches, Regex.Split und Regex.Replace.

Leisten Sie Ihren Beitrag

Die in diesem Artikel behandelten Features sind in Visual Studio 11 Beta verfügbar. Diese entspricht unseren hohen Standards für Vorabversionen von Software, sodass wir sie in Produktionsumgebungen unterstützen. Sie können die Betaversion von dieser Website herunterladen: bit.ly/9JWDT9. Da es sich um eine Vorabversion handelt, sind wir auf Ihre Meinung angewiesen, ob die relevanten Probleme Ihrer Meinung nach behandelt wurden (connect.microsoft.com/visualstudio) oder ob Sie Ideen zu neuen Themen oder Features haben (visualstudio.uservoice.com). Sie können ebenfalls den Blog meines Teams unter blogs.msdn.com/b/bclteam, abonnieren, damit Sie über kommende Änderungen oder Ankündigungen rechtzeitig informiert werden.

Immo Landwerth ist Program Manager im CLR-Team von Microsoft und arbeitet in den Bereichen Microsoft .NET Framework-Basisklassenbibliothek (BCL), API-Design und portierbare Klassenbibliotheken. Sie können ihn über den BCL-Teamblog unter blogs.msdn.com/b/bclteam erreichen.

Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: Nicholas Blumhardt, Greg Paperin, Daniel Plaisted, Evgeny Roubinchtein, Alok Shriram, Chris Szurgot, Stephen Toub und Mircea Trofin.