CLR Inside Out
What’s New in the Base Class Libraries in .NET Framework 4
Just about everyone who uses Microsoft .NET uses the Base Class Libraries (BCL).When we make the BCL better, almost every managed developer benefits.
This column will focus on the new additions to the BCL in .NET 4 beta 1.
Three of the additions have already been covered in previous articles -- I'll start with a brief review of these:
-
Support for code contracts
-
Parallel extensions (tasks, concurrent collections, and coordination data structures)
-
Support for tuples
Then, in the main part of the article, I'll talk about three new additions we haven't written about yet:
-
File IO improvements
-
Support for memory mapped files
-
A sorted set collection
There isn't room to describe all of the new BCL improvements in this article, but you can read about them in upcoming posts on the BCL team blog at blogs.msdn.com/bclteam.
These include:
-
Support for arbitrarily large integers
-
Generic variance annotations on interfaces and delegates
-
Support for accessing 32-bit and 64-bit registry views and creating volatile registry keys
-
Globalization data updates
-
Improved System.Resourcesresource lookup fallback logic
-
Compression improvements
We're also planning to include some additional features and improvements for beta 2 that you will be able to read about on the BCL team blog around the time when beta 2 ships.
Code Contracts
One of the major new features added to the BCL in the .NET Framework 4 is code contracts.
This new library provides a languageagnostic way to specify pre-conditions, post-conditions and object invariants in your code.
You'll find more information on code contracts in Melitta Andersen's August 2009 MSDN Magazine CLR Inside Out column.
You should also take a look at the Code Contracts DevLabs site at msdn.microsoft.com/en-us/devlabs/dd491992.aspx and on the BCL team blog at blogs.msdn.com/bclteam.
Parallel Extensions
As multi-core processors have become more important on the client, and massively parallel servers have become more wide-spread, it's more important than ever to make it easy for programmers to use all of these processors.
Another major new addition to the BCL in .NET 4 is the Parallel Extensions (PFX) feature that is being delivered by the Parallel Computing Platform team.
PFX include the Task Parallel Library (TPL), Coordination Data Structures, Concurrent Collections and Parallel LINQ (PLINQ) -- all of which will make it easier to write code that can take advantage of multi-core machines.
More background on PFX can be found in Stephen Toub and Hazim Shafi's article "Improved Support For Parallelism In The Next Version Of Visual Studio" in the October 2008 issue of MSDN Magazine, available online at msdn.microsoft.com/en-us/magazine/cc817396.aspx.
The PFX team blog at blogs.msdn.com/pfxteam is also a great source of information on PFX.
Tuples
Another addition to the BCL in .NET 4 is support for tuples,which are similar to anonymous classes that you can create on the fly.
A tuple is a data structure used in many functional and dynamic languages, such as F# and Iron Python.
By providing common tuple types in the BCL, we are helping to better facilitate language interoperability.
Many programmers find tuples to be convenient, particularly for returning multiple values from methods -- so even C# or Visual Basic developers will find them useful.
Matt Ellis's July 2009 MSDN Magazine CLR Inside Out column discusses the new support for tuples in .NET 4.
File IO Improvements
One of the new additions to .NET 4 that we haven't written about in detail is the new methods on System.IO.File for reading and writing text files.
Since .NET 2.0, if you needed to read the lines of a text file, you could call the File.ReadAllLines method, which returns a string array of all the lines in the file.
The following code uses File.ReadAllLines to read the lines of a text file and writes the length of the line along with the line itself to the console:
string[] lines = File.ReadAllLines("file.txt");
foreach (var line in lines) {
Console.WriteLine("Length={0}, Line={1}", line.Length, line);
}
Unfortunately, there is a subtle issue with this code.
The issue stems from the fact that ReadAllLines returns an array.
Before ReadAllLines can return, it must read all lines and allocate an array to return.
This isn’t too bad for relatively small files, but it can be problematic for large text files that have millions of lines.
Imagine opening a text file of the phone book or a 900-page novel.
ReadAll- Lines will block until all lines are loaded into memory.
Not only is this an inefficient use of memory, but it also delays the processing of the lines, because you can’t access the first line until all lines have been read into memory.
To work around the issue, you could use a TextReader to open the file and then read the lines of the file into memory, one line at a time.
This works, but it’s not as simple as calling File.ReadAllLines:
using (TextReader reader = new StreamReader("file.txt")) {
string line;
while ((line = reader.ReadLine()) != null) {
Console.WriteLine("Length={0}, Line={1}", line.Length, line);
}
}
In .NET 4, we've added a new method to File named ReadLines (as opposed to ReadAllLines) that returns IEnumerable<string> instead of string[].
This new method is much more efficient because it does not load all of the lines into memory at once; instead, it reads the lines one at a time.
The following code uses File.Read-Lines to efficiently read the lines of a file and is just as easy to use as the less efficientFile.ReadAllLines:
IEnumerable<string> lines = File.ReadLines(@"verylargefile.txt");
foreach (var line in lines) {
Console.WriteLine("Length={0}, Line={1}", line.Length, line);
}
Note that the call to File.ReadLines returns immediately.
You no longer have to wait until all of the lines are read into memory before you can iterate through the lines.
The iteration of the foreach loop actually drives the reading of the file.
Not only does this significantly improve the perceived performance of the code, because you can start processing the lines as they’re being read, it’s also much more efficient because the lines are being read one at a time.
Using File.
ReadLines has an added benefit in that it allows you to break out of the loop early if necessary, without wasting time reading additional lines you don’t care about.
We've also added new overloads to File.WriteAllLines that take an IEnumerable<string> parameter similar to the existing overloads that take a string[] parameter.
And we added a new method called AppendAllLines that takes an IEnumerable<string> for appending lines to a file.
These new methods allow you to easily write or append lines to a file without having to pass in an array.
This means if you have a collection of strings, you can pass it directly to these methods without having to first convert it into a string array.
There are other places in the BCL that would benefit from the use of IEnumerable<T> over arrays.
Take the file system enumeration APIs, for example.
In previous versions of the framework, to get the files in a directory, you would call a method like DirectoryInfo.
GetFiles, which returns an array of FileInfo objects.
You could then iterate through the FileInfo objects to get information about the files, such as the Name and Length of each file.
The code to do that might look like this:
DirectoryInfo directory = new DirectoryInfo(@"\\share\symbols");
FileInfo[] files = directory.GetFiles();
foreach (var file in files) {
Console.WriteLine("Name={0}, Length={1}", file.Name, file.Length);
}
There are two issues with this code.
The first issue shouldn’t surprise you; just as with File.ReadAllLines, it stems from the fact that GetFiles returns an array.
Before GetFiles can return, it must retrieve the complete list of files in the directory from the file system and then allocate an array to return.
This means you have to wait for all files to be retrieved before getting the first results and this is an inefficient use of memory.
If the directory contains a million files, you have to wait until all 1,000,000 files have been retrieved and a million-length array has been allocated.
The second issue with the above code is a bit more subtle.
FileInfo instances are created by passing a file path to FileInfo’s constructor.
Properties on FileInfo such as Length and CreationTime are initialized the first time one of the properties is accessed.
When a property is first accessed, FileInfo.Refresh is called which calls into the operating system to retrieve the file’s properties from the file system.
This avoids a call to retrieve the data if the properties are never used, and when they are used, it helps to ensure the data isn’t stale when first accessed.
This works great for one-off instances of FileInfo, but it can be problematic when enumerating the contents of a directory because it means that additional calls to the file system will be made to get the file properties.
These additional calls can hinder performance when looping through the results.
This is especially problematic if you are enumerating the contents of a remote file share, because it means an additional round-trip call to the remote machine across the network.
In .NET 4, we have addressed both of these issues.
To address the first issue, we have added new methods on Directory and DirectoryInfo that return IEnumerable<T> instead of arrays.
Like File.ReadLines, these new IEnumerable<T>-based methods are much more efficient than the older array-based equivalents.
Consider the following code that has been updated to use .NET 4's DirectoryInfo.
EnumerateFiles method instead of DirectoryInfo.GetFiles:
DirectoryInfo directory = new DirectoryInfo(@"\\share\symbols");
IEnumerable<FileInfo> files = directory.EnumerateFiles();
foreach (var file in files) {
Console.WriteLine("Name={0}, Length={1}", file.Name, file.Length);
}
Unlike GetFiles, EnumerateFiles does not have to block until all of the files are retrieved, nor does it have to allocate an array.
Instead, it returns immediately, and you can process each file as it is returned from the file system.
To address the second issue, DirectoryInfo now makes use of data that the operating system already provides from the file system during enumeration.
The underlying Win32 functions that Directory-Info calls to get the contents of the file system during enumeration actually include data about each file, such as the length and creation time.
We now use this data when initializing the FileInfo and DirectoryInfo instances returned from both the older array-based and new IEnumerable<T>-basedmethods on DirectoryInfo.
This means that in the preceding code, there are no additional underlying calls to the file system to retrieve the length of the file when file.Length is called, since this data has already been initialized.
Together, the new IEnumerable<T>-based methods on File and Directory enable some interesting scenarios.
Consider the following code:
var errorlines =
from file in Directory.EnumerateFiles(@"C:\logs", "*.log")
from line in File.ReadLines(file)
where line.StartsWith("Error:", StringComparison.OrdinalIgnoreCase)
select string.Format("File={0}, Line={1}", file, line);
File.WriteAllLines(@"C:\errorlines.log", errorlines);
This uses the new methods on Directory and File, along with LINQ, to efficiently find files that have a .log extension, and specifically lines in those files that start with "Error:".
The query then projects the results into a new sequence of strings, with each string formatted to show the path to the file and the error line.
Finally, File.
WriteAllLines is used to write the error lines to a new file named "errorlines.log," without having to convert error lines into an array.
The great thing about this code is that it is very efficient.
At no time have we pulled the entire list of files into memory, nor have we pulled in the entire contents of a file into memory.
No matter if C:\logs contains 10 files or a million files, and no matter if the files contain 10 lines or a million lines, the above code is just as efficient, using a minimal amount of memory.
Memory-Mapped Files
Support for memory-mapped files is another new feature in the .NET Framework 4.
Memory-mapped files can be used to edit large files or create shared memory for inter-process communication (IPC).
Memory-mapped files allow you to map a file into the address space of a process.
Once mapped, an application can simply read or write to memory to access or modify the contents of the file.
Since the file is accessed through the operating system’s memory manager, the file is automatically partitioned into a number of pages that are paged in and paged out of memory as needed.
This makes working with large files easier, as you don’t have to handle the memory management yourself.
It also allows for complete random access to a file without the need for seeking.
Memory-mapped files can be created without a backing file.
Such memory-mapped files are backed by the system paging file (only if one exists and the contents needs to be paged out of memory).
Memory-mapped files can be shared across multiple processes, which means they’re a great way to create shared memory for interprocess communication.
Each mapping can have a name associated with it that other processes can use for opening the same memory-mapped file.
To use memory-mapped files, you must first create a Memory- MappedFile instance using one of the following static factory methods on the System.IO.MemoryMappedFiles.MemoryMappedFile class:
-
CreateFromFile
-
CreateNew
-
CreateOrOpen
-
OpenExisting
After this, you can create one or more views that actually maps the file into the process’s address space.
Each view can map all or part of the memory mapped file, and views can overlap.
Using more than one view may be necessary if the file is greater than the size of the process’s logical memory space available for mapping (2GB on a 32-bit machine).
You can create a view by calling either the CreateViewStream or CreateViewAccessor methods on the MemoryMappedFile object.
CreateViewStream returns an instance of MemoryMappedFileViewStream, which inherits from System.IO.UnmanagedMemoryStream.
This can be used like any other Stream in the framework.
CreateViewAccessor, on the other hand, returns an instance of MemoryMappedFileViewAccessor, which inherits from the new System.IO.UnmanagedMemoryAccessorclass.
UnmanagedMemoryAccessor enables random access, whereas UnmanagedMemoryStream enables sequential access.
The following sample shows how to use memory-mapped files to create shared memory for IPC.
Process 1, as shown in Figure 1, creates a new MemoryMappedFile instance using the CreateNew method specifying the name of the memory mapped file along with the capacity in bytes.
This will create a memory-mapped file backed by the system paging file.
Note that internally, the specified capacity is rounded up to the next multiple of the system’s page size (if you’re curious you can get the system page size from Environment.System- PageSize, which is new in .NET 4).
Next, a view stream is created using CreateViewStream and “Hello Word!” is written to the stream using an instance of BinaryWriter.
Then the second process is started.
Process 2, as shown in Figure 2, opens the existing memory mapped file using the OpenExisting method, specifying the appropriate name of the memory mapped file.
From there, a view stream is created and the string is read using an instance of BinaryReader.
Figure 1 Process 1
using (varmmf = MemoryMappedFile.CreateNew("mymappedfile", 1000))
using (var stream = mmf.CreateViewStream()) {
var writer = new BinaryWriter(stream);
writer.Write("Hello World!");
varstartInfo = new ProcessStartInfo("process2.exe");
startInfo.UseShellExecute = false;
Process.Start(startInfo).WaitForExit();
}
Figure 2 Process 2
using (varmmf = MemoryMappedFile.OpenExisting("mymappedfile"))
using (var stream = mmf.CreateViewStream()) {
var reader = new BinaryReader(stream);
Console.WriteLine(reader.ReadString());
}
SortedSet<T>
In addition to the new collections in System.Collections.Concurrent (part of PFX), the .NET Framework 4 includes a new set collection in System.Collections.Generic, called SortedSet<T>.
Like HashSet<T>, which was added in .NET 3.5, SortedSet<T> is a collection of unique elements, but unlike HashSet<T>, SortedSet<T> keeps the elements in sorted order.
SortedSet<T> is implemented using a self-balancing red-black tree that gives a performance complexity of O(log n) for insert, delete, and lookup.
HashSet<T>, on the other hand, provides slightly better performance of O(1) for insert, delete and lookup.
If you just need a general purpose set, in most cases you should use HashSet<T>.
However, if you need to keep the elements in sorted order, get the subset of elements in a particular range, or get the min or max element, you'll want to use SortedSet<T>.
The following code demonstrates the use of SortedSet<T> with integers:
var set1 = new SortedSet<int>() { 2, 5, 6, 2, 1, 4, 8 };
bool first = true;
foreach (var i in set1) {
if (first) {
first = false;
}
else {
Console.Write(",");
}
Console.Write(i);
}
// Output: 1,2,4,5,6,8
The set is created and initialized using C#'s collection initializer syntax.
Note that the integers are added to the set in no particular order.
Also note that 2 is added twice.
It should be no surprise that when looping through set1's elements, we see that the integers are in sorted order and that the set contains only one 2.
Like HashSet<T>, SortedSet<T>'s Add method has a return type of bool that can be used to determine whether the item was successfully added (true) or if it wasn't added because the set already contains the item (false).
Figure 3 shows how to get the max and min elements within the set and get a subset of elements in a particular range.
Figure 3 Getting Max, Min and Subset View of Elements
var set1 = new SortedSet<int>() { 2, 5, 6, 2, 1, 4, 8 };
Console.WriteLine("Min: {0}", set1.Min);
Console.WriteLine("Max: {0}", set1.Max);
var subset1 = set1.GetViewBetween(2, 6);
Console.Write("Subset View: ");
bool first = true;
foreach (var i in subset1) {
if (first) {
first = false;
}
else {
Console.Write(",");
}
Console.Write(i);
}
// Output:
// Min: 1
// Max: 8
// Subset View: 2,4,5,6
The GetViewBetween method returns a view of the original set.
This means that any changes made to the view will be reflected in the original.
For example, if 3 is added to subset1 in the code above, it's really added to set1.
Note that you cannot add items to a view outside of the specified bounds.
For example, attempting to add a 9 to subset1 in the code above will result in an Argument-Exception because the view is between 2 and 6.
Try It Out
The new BCL features discussed in this column are a sampling of the new functionality available in the .NET Framework 4.
These features are available in preview form as part of the .NET Framework 4 beta 1, which is available for download along with Visual Studio 2010 beta 1 at msdn.microsoft.com/en-us/netframework/dd819232.aspx.
Download the beta, try out the new functionality, and let us know what you think at connect.microsoft.com/VisualStudio/content/content.aspx?ContentID=12362.
Be sure to also keep an eye on the BCL team blog for upcoming posts on some of the other BCL additions and an announcement on what’s new in beta 2.
Justin Van Patten
is a program manager on the CLR team at Microsoft, where he works on the Base Class Libraries.
You can reach him via the BCL team blog at blogs.msdn.com/bclteam.
|
CLR Inside Out
Neuigkeiten in den Basisklassenbibliotheken in .NET Framework 4
Praktisch jeder, der Microsoft .NET verwendet, nutzt die Base Class Libraries (BCL). Von jeder Verbesserung der BCL profitiert praktisch jeder Entwickler.
Diese Kolumne befasst sich mit den neuen Erweiterungen der BCL in .NET 4 Beta 1.
Drei der Ergänzungen wurden bereits in früheren Artikeln behandelt: Ich beginne mit einer kurzen Übersicht darüber:
-
Unterstützung für Codeverträge
-
Parallele Erweiterungen (Aufgaben, gleichzeitige Auflistungen und Koordinationsdatenstrukturen)
-
Unterstützung für Tupel
Im Hauptteil des Artikels werde ich anschließend über drei neue Erweiterungen sprechen, über die noch nicht geschrieben wurde:
-
Datei-E/A-Verbesserungen
-
Unterstützung für im Speicher abgebildete Dateien
-
Eine sortierte Gruppenauflistung
Es ist nicht genügend Platz vorhanden, um alle neuen BCL-Verbesserungen in diesem Artikel zu beschreiben, aber Sie können in kommenden Beiträgen im BCL-Teamblog unter "blogs.msdn.com/bclteam" mehr darüber erfahren.
Diese umfassen:
-
Unterstützung für beliebig große ganze Zahlen
-
Anmerkungen zu generischer Varianz in Schnittstellen und Delegaten
-
Unterstützung für den Zugriff auf 32-Bit- und 64-Bit-Registrierungsansichten und Erstellung temporärer Registrierungsschlüssel
-
Globalisierungsdatenaktualisierungen
-
Verbesserte System.Resourcesresource-Lookup-Zugriffslogik
-
Komprimierungsverbesserungen
Es sind auch einige zusätzliche Funktionen und Verbesserungen für Beta 2 vorgesehen, zu denen Sie weitere Informationen im BCL-Teamblog nachlesen können, wenn Beta 2 ausgeliefert wird.
Code Contracts
Eine der wichtigsten neuen Funktionen, die der BCL in .NET Framework 4 hinzugefügt wurde, sind Codeverträge.
Diese neue Bibliothek bietet eine sprachenagnostische Möglichkeit zur Angabe von Vorbedingungen, Nachbedingungen und Objektinvarianten in Ihrem Code.
Weitere Informationen zu Codeverträgen finden Sie in der CLR Inside Out-Kolumne des MSDN Magazine vom August 2009 von Melitta Andersen.
Sie sollten auch einen Blick auf die DevLabs-Website zu Codeverträgen unter msdn.microsoft.com/devlabs/dd491992.aspx und den BCL-Teamblog unter blogs.msdn.com/bclteam werfen.
Parallele Erweiterungen
Da Mehrkernprozessoren auf dem Client immer wichtiger werden und parallele Server immer weiter verbreitet sind, ist es für Programmierer wichtiger als je zuvor, all diese Prozessoren zu verwenden.
Eine andere wichtige Ergänzung der BCL in .NET 4 ist die PFX-Funktion (Parallel Extensions), die vom Parallel Computing Platform-Team bereitgestellt wird.
PFX umfasst die Task Parallel Library (TPL), Coordination Data Structures, Concurrent Collections und Parallel LINQ (PLINQ), die alle das Schreiben von Code erleichtern, der Computer mit mehreren Prozessorkernen nutzen kann.
Weitere Hintergrundinformationen zu PFX finden Sie im Artikel "Verbesserte Unterstützung für Parallelität in der nächsten Version von Visual Studio" von Stephen Toub und Hazim Shafi in der Ausgabe des MSDN Magazine vom Oktober 2008, verfügbar online unter msdn.microsoft.com/magazine/cc817396.aspx.
Der PFX-Teamblog unter blogs.msdn.com/pfxteam ist ebenfalls eine hervorragende Informationsquelle zu PFX.
Tupel
Ein weitere Ergänzung der BCL in .NET 4 ist die Unterstützung für Tupel, die anonymen Klassen ähnlich sind, die Sie zur Laufzeit erstellen können.
Ein Tupel ist eine Datenstruktur, die in vielen funktionalen und dynamischen Sprachen wie F# und Iron Python verwendet wird.
Durch Bereitstellung von häufig verwendeten Tupeltypen in der BCL wird die sprachübergreifende Interoperabilität vereinfacht.
Viele Programmierer finden Tupel von Vorteil, insbesondere für das Zurückgeben mehrerer Werte von Methoden; sogar C# oder Visual Basic-Entwickler werden sie hilfreich finden.
In der CLR Inside Out-Kolumne von Matt Ellis in der Ausgabe des MSDN Magazine vom Juli 2009 wird die neue Unterstützung von Tupeln in .NET 4 besprochen.
Datei-E/A-Verbesserungen
Eine der Neuerungen in .NET 4, die wir noch nicht im Detail beschrieben haben, sind die neuen Methoden in System.IO.File zum Lesen und Schreiben von Textdateien.
Seit .NET 2.0 konnten Sie, wenn Sie die Zeilen einer Textdatei lesen wollten, die File.ReadAllLines-Methode aufrufen, die ein Zeichenfolgenarray aller Zeilen in der Datei zurückgibt.
Der folgende Code verwendet File.ReadAllLines, um die Zeilen der Textdatei zu lesen, und schreibt die Länge der Zeile zusammen mit der Zeile selbst in die Konsole:
string[] lines = File.ReadAllLines("file.txt");
foreach (var line in lines) {
Console.WriteLine("Length={0}, Line={1}", line.Length, line);
}
Leider gibt es ein kleines Problem bei diesem Code.
Das Problem rührt daher, dass ReadAllLines ein Array zurückgibt.
Bevor ReadAllLines einen Wert zurückgeben kann, müssen alle Zeilen gelesen und ein Array für die Rückgabe zugeordnet werden.
Das ist bei verhältnismäßig kleinen Dateien kein Problem, kann jedoch für große Textdateien, die Millionen von Zeilen aufweisen, problematisch sein.
Angenommen, Sie öffnen eine Textdatei des Telefonbuchs oder ein Buch mit 900 Seiten.
ReadAllLines ist solange blockiert, bis alle Zeilen in den Speicher geladen wurden.
Dies ist nicht nur eine ineffiziente Verwendung des Arbeitsspeichers, sondern verzögert auch die Verarbeitung der Zeilen, da Sie erst auf die erste Zeile zugreifen können, wenn alle Zeilen in den Arbeitsspeicher gelesen wurden.
Um das Problem zu umgehen, können Sie einen TextReader verwenden, um die Datei zu öffnen und die Zeilen der Datei zeilenweise in den Arbeitsspeicher zu lesen.
Dies funktioniert zwar, ist aber nicht so leicht wie das Aufrufen von File.ReadAllLines:
using (TextReader reader = new StreamReader("file.txt")) {
string line;
while ((line = reader.ReadLine()) != null) {
Console.WriteLine("Length={0}, Line={1}", line.Length, line);
}
}
In .NET 4 wurde eine neue Methode mit dem Namen ReadLines (im Gegensatz zu ReadAllLines) hinzugefügt, die IEnumerable<string> anstelle von string[] zurückgibt.
Diese neue Methode ist viel effizienter, da sie nicht alle Zeilen gleichzeitig in den Speicher lädt; die Zeilen werden stattdessen einzeln gelesen.
Der folgende Code verwendet File.Read-Lines, um die Zeilen einer Datei effizient zu lesen und ist genauso einfach zu verwenden wie der weniger effiziente Befehl File.ReadAllLines:
IEnumerable<string> lines = File.ReadLines(@"verylargefile.txt");
foreach (var line in lines) {
Console.WriteLine("Length={0}, Line={1}", line.Length, line);
}
Beachten Sie, dass der Aufruf von File.ReadLines sofort einen Wert zurückgibt.
Sie müssen nicht mehr warten, bis alle Zeilen in Speicher gelesen wurden, bevor Sie die Zeilen durchlaufen können.
Die Iteration der Foreach-Schleife löst tatsächlich das Lesen der Datei aus.
Dadurch wird nicht nur die wahrgenommene Leistung des Codes erheblich verbessert, weil Sie mit der Verarbeitung der Zeilen beginnen können, während diese gelesen werden, dies ist auch viel effizienter, da die Zeilen einzeln gelesen werden.
Die Verwendung von File.
ReadLines hat den weiteren Vorteil, dass Sie die Schleife, falls erforderlich, frühzeitig unterbrechen können, ohne unnötig Zeit für das Lesen zusätzlicher Zeilen zu verschwenden, an denen Sie nicht interessiert sind.
File.WriteAllLines wurden auch neue Überladungen hinzugefügt, die einen IEnumerable<string >-Parameter verwenden, ähnlich wie vorhandene Überladungen, die einen string[]-Parameter verwenden.
Und es wurde eine neue Methode mit dem Namen "AppendAllLines" hinzugefügt, die einen IEnumerable<string >-Parameter für das Anfügen von Zeilen an eine Datei akzeptiert.
Mit diesen neuen Methoden können Sie problemlos Zeilen in eine Datei schreiben oder daran anfügen, ohne ein Array übergeben zu müssen.
Das heißt, wenn Sie eine Auflistung von Zeichenfolgen haben, können Sie diese direkt an diese Methoden übergeben, ohne sie zuerst in ein Zeichenfolgenarray konvertieren zu müssen.
Es gibt andere Stellen in der BCL, die von Verwendung von IEnumerable<T> anstelle von Arrays profitieren würden.
Beispielsweise die Enumeration-APIs des Dateisystems.
In früheren Versionen des Frameworks musste eine Methode wie DirectoryInfo aufgerufen werden, um die Dateien in einem Verzeichnis abzurufen.
GetFiles, das ein Array von FileInfo-Objekten zurückgibt.
Sie könnten dann die FileInfo-Objekte durchlaufen, um Informationen zu den Dateien abzurufen, z. B. den Namen und die Länge der einzelnen Dateien.
Der Code dazu könnte wie folgt aussehen:
DirectoryInfo directory = new DirectoryInfo(@"\\share\symbols");
FileInfo[] files = directory.GetFiles();
foreach (var file in files) {
Console.WriteLine("Name={0}, Length={1}", file.Name, file.Length);
}
Es gibt zwei Probleme mit diesem Code.
Das erste Problem dürfte Sie nicht überraschen: Genau wie bei File.ReadAllLines besteht das Problem darin, dass GetFiles ein Array zurückgibt.
Bevor GetFiles einen Wert zurückgeben kann, muss erst die vollständige Liste von Dateien im Verzeichnis aus dem Dateisystem abgerufen und ein Array für die Rückgabe reserviert werden.
Dies bedeutet, dass Sie warten müssen, bis alle Dateien abgerufen wurden, bevor Sie die ersten Ergebnisse erhalten. Dies stellt eine ineffiziente Verwendung von Arbeitsspeicher dar.
Wenn das Verzeichnis eine Million Dateien enthält, müssen Sie warten, bis alle 1.000.000 Dateien abgerufen haben und ein Array mit der Länge für eine Million zugewiesen wurde.
Das zweite Problem mit dem obigen Code ist etwas subtiler.
FileInfo-Instanzen werden erstellt, indem ein Dateipfad an den Konstruktor von FileInfo übergeben wird.
Eigenschaften in FileInfo wie Length und CreationTime werden initialisiert, wenn zum ersten Mal auf eine der Eigenschaften zugegriffen wird.
Wenn zum ersten Mal auf eine Eigenschaft zugegriffen wird, wird FileInfo.Refresh aufgerufen, wodurch ein Aufruf im Betriebssystem zum Abrufen der Eigenschaften der Datei aus dem Dateisystem ausgelöst wird.
Dadurch wird verhindert, dass ein Aufruf die Datei abruft, wenn die Eigenschaften nie verwendet werden; wenn sie verwendet werden, kann dadurch sichergestellt werden, dass die Dateien beim ersten Zugriff nicht veraltet sind.
Dies funktioniert hervorragend für einmalige Instanzen von FileInfo, kann aber beim Auflisten des Inhalts eines Verzeichnisses problematisch sein, weil dies bedeutet, dass zusätzliche Aufrufe an das Dateisystem erfolgen müssen, um die Dateieigenschaften abzurufen.
Diese zusätzlichen Aufrufe können beim Durchlaufen der Ergebnisse die Leistung beeinträchtigen.
Dies ist insbesondere dann problematisch, wenn Sie den Inhalt einer Remotedateifreigabe auflisten, da dies einen zusätzlichen Roundtrip-Aufruf an den Remotecomputer über das Netzwerk bedeutet.
In .NET 4 wurden diese beiden Probleme behoben.
Um das erste Problem zu beheben, wurden neuen Methoden für Directory und DirectoryInfo hinzugefügt, die IEnumerable<T> anstelle von Arrays zurückgeben.
Genau wie File.ReadLines sind die neuen auf IEnumerable<T>-basierten Methoden wesentlich effizienter als die älteren arraybasierten Entsprechungen.
Sehen Sie sich folgenden Code an, der so aktualisiert wurde, dass DirectoryInfo von .NET 4 verwendet wird.
EnumerateFiles-Methode anstelle von DirectoryInfo.GetFiles:
DirectoryInfo directory = new DirectoryInfo(@"\\share\symbols");
IEnumerable<FileInfo> files = directory.EnumerateFiles();
foreach (var file in files) {
Console.WriteLine("Name={0}, Length={1}", file.Name, file.Length);
}
Im Gegensatz zu GetFiles, wird EnumerateFiles nicht blockiert, bis alle Dateien abgerufen wurden, und es muss auch kein Array zugewiesen werden.
Stattdessen wird sofort ein Wert zurückgegeben, und Sie können die einzelnen Dateien direkt nach der Rückgabe aus dem Dateisystem verarbeiten.
Um das zweite Problem zu beheben, verwendet DirectoryInfo nun Daten, die das Betriebssystem bereits aus dem Dateisystem während der Auflistung bereitstellt.
Die zugrunde liegenden Win32-Funktionen, die DirectoryInfo aufruft, um den Inhalt des Dateisystems während der Auflistung abzurufen, enthalten tatsächlich Daten zu jeder Datei, z. B. die Länge und die Erstellungszeit.
Diese Daten werden nun bei der Initialisierung der FileInfo- und DirectoryInfo-Instanzen verwendet, die beide aus älteren arraybasierten und den neuen IEnumerable<T>-basierten Methoden in DirectoryInfo zurückgegeben werden.
Dies bedeutet, dass im obigen Code keine zugrunde liegenden zusätzlichen Aufrufe für das Dateisystem zum Abrufen der Länge der Datei vorhanden sind, wenn file.Length aufgerufen wird, da diese Daten bereits initialisiert wurden.
Zusammen ermöglichen die neuen IEnumerable<T>-basierten Methode für File und Directory interessante Szenarien.
Betrachten Sie den folgenden Code:
var errorlines =
from file in Directory.EnumerateFiles(@"C:\logs", "*.log")
from line in File.ReadLines(file)
where line.StartsWith("Error:", StringComparison.OrdinalIgnoreCase)
select string.Format("File={0}, Line={1}", file, line);
File.WriteAllLines(@"C:\errorlines.log", errorlines);
Dabei werden die neuen Methoden für Directory und File zusammen mit LINQ verwendet, um effizient nach Dateien suchen, die die Erweiterung ".log" aufweisen, und speziell nach Zeilen in diesen Dateien, die mit "Error:" beginnen.
Die Abfrage projiziert anschließend die Ergebnisse in eine neue Sequenz von Zeichenfolgen, wobei jede Zeichenfolge so formatiert ist, dass der Pfad zu der Datei und zu der Fehlerzeile angezeigt wird.
File.
WriteAllLines wird verwendet, um die Fehlerzeilen in eine neue Datei mit dem Namen "errorlines.log", zu schreiben, ohne Fehlerzeilen in ein Array zu konvertieren.
Der große Vorteil dieses Codes liegt darin, dass es sehr effizient ist.
Zu keinem Zeitpunkt wurde weder die gesamte Liste von Dateien in den Arbeitsspeicher gezogen, noch wurde der gesamte Inhalt einer Datei in den Speicher geladen.
Der obige Code ist dank einer minimalen Verwendung von Speicherplatz immer gleich effizient, unabhängig davon, ob C:\logs 10 Dateien oder eine Million Dateien enthält, und unabhängig davon, ob die Dateien 10 Zeilen oder eine Million Zeilen enthalten.
Im Speicher abgebildete Dateien
Die Unterstützung von im Speicher abgebildeten Dateien ist eine weitere neue Funktion in .NET Framework 4.
Im Speicher abgebildete Dateien können verwendet werden, um größere Dateien zu bearbeiten oder freigegebenen Speicher für die prozessübergreifende Kommunikation (IPC) zu erstellen.
Mit im Speicher abgebildeten Dateien können Sie eine Datei dem Adressraum eines Prozesses zuordnen.
Nach der Zuordnung kann eine Anwendung ganz einfach einen Lese- oder Schreibvorgang für den Speicher ausführen, um auf den Inhalt der Datei zuzugreifen oder diesen zu ändern.
Da auf die Datei über den Speicher-Manager des Betriebssystems zugegriffen wird, wird die Datei automatisch in eine Anzahl von Seiten unterteilt, die bei Bedarf im ausgelagert werden.
Dies erleichtert das Arbeiten mit großen Dateien, da Sie sich nicht selbst um die Speicherverwaltung kümmern müssen.
Dadurch wird auch der vollständig zufällige Zugriff auf eine Datei ermöglicht, ohne diese suchen zu müssen.
Im Speicher abgebildete Dateien können ohne eine Wiederherstellungsdatei erstellt werden.
Solche im Speicher abgebildeten Dateien werden durch die Systemauslagerungsdatei gesichert (nur falls eine solche Datei vorhanden ist und der Inhalt aus dem Speicher ausgelagert werden muss).
Im Speicher abgebildete Dateien können über mehrere Prozesse hinweg freigegeben werden, was bedeutet, dass sie eine hervorragende Möglichkeit zum Erstellen von freigegebenem Speicher für die prozessübergreifende Kommunikation bieten.
Jeder Zuordnung kann ein Name zugewiesen werden, den andere Prozesse zum Öffnen der gleichen im Speicher abgebildeten Datei verwenden können.
Um im Speicher abgebildete Dateien verwenden zu können, müssen Sie zunächst eine Memory- MappedFile-Instanz mithilfe einer der folgenden statischen Factorymethoden in der System.IO.MemoryMappedFiles.MemoryMappedFile-Klasse erstellen:
-
CreateFromFile
-
CreateNew
-
CreateOrOpen
-
OpenExisting
Danach können Sie eine oder mehrere Ansichten erstellen, durch die die Datei tatsächlich dem Adressraum des Prozesses zugeordnet wird.
Jede Ansicht kann die gesamte oder einen Teil der im Speicher abgebildeten Datei zuordnen, und Ansichten können überlappen.
Die Verwendung mehrerer Ansichten ist möglicherweise erforderlich, wenn die Datei größer als die Größe des logischen Speicherplatzes des Prozesses, der für die Zuordnung verfügbar ist (2 GB auf einem 32-Bit-Computer).
Sie können eine Ansicht erstellen, indem Sie entweder die CreateViewStream- oder die CreateViewAccessor-Methode für das MemoryMappedFile-Objekt aufrufen.
CreateViewStream gibt eine Instanz von MemoryMappedFileViewStream zurück, die von System.IO.UnmanagedMemoryStream erbt.
Diese kann wie jeder andere Stream im Framework verwendet werden.
CreateViewAccessor gibt hingegen eine Instanz von MemoryMappedFileViewAccessor zurück, die von der neuen System.IO.UnmanagedMemoryAccessor-Klasse erbt.
UnmanagedMemoryAccessor ermöglicht einen zufälligen Zugriff, wohingegen UnmanagedMemoryStream einen sequenziellen Zugriff ermöglicht.
Das folgende Beispiel veranschaulicht die Verwendung von im Speicher abgebildeten Dateien zum Erstellen von freigegebenen Speicher für IPC.
Durch Prozess 1 wird, wie in Abbildung 1 dargestellt, eine neue MemoryMappedFile-Instanz mithilfe der CreateNew-Methode erstellt, die den Namen der im Speicher abgebildeten Datei zusammen mit der Kapazität in Bytes angibt.
Dadurch wird eine im Speicher abgebildete Datei erstellt, die von der Systemauslagerungsdatei gesichert wird.
Beachten Sie, dass die angegebene Kapazität intern auf das nächste Vielfache der Systemauslagerungsdatei aufgerundet wird (wenn es Sie interessiert, können Sie die Auslagerungsgröße des System aus Environment.System- PageSize abrufen, die neu in .NET 4 ist).
Als Nächstes wird ein Ansichtsstream mithilfe von CreateViewStream erstellt, und "Hello World!" wird mithilfe einer Instanz von BinaryWriter in den Stream geschrieben.
Anschließend wird der zweite Prozess gestartet.
Durch Prozess 2 wird, wie in Abbildung 2 dargestellt, die vorhandene im Speicher abgebildete Datei mithilfe der OpenExisting-Methode geöffnet, wodurch der entsprechende Name der im Speicher abgebildeten Datei angegeben wird.
Von dort aus wird ein Ansichtsstream erstellt, und die Zeichenfolge wird mithilfe einer Instanz von BinaryReader gelesen.
Abbildung 1 Prozess 1
using (varmmf = MemoryMappedFile.CreateNew("mymappedfile", 1000))
using (var stream = mmf.CreateViewStream()) {
var writer = new BinaryWriter(stream);
writer.Write("Hello World!");
varstartInfo = new ProcessStartInfo("process2.exe");
startInfo.UseShellExecute = false;
Process.Start(startInfo).WaitForExit();
}
Abbildung 2 Prozess 2
using (varmmf = MemoryMappedFile.OpenExisting("mymappedfile"))
using (var stream = mmf.CreateViewStream()) {
var reader = new BinaryReader(stream);
Console.WriteLine(reader.ReadString());
}
SortedSet<T>
Neben den neuen Auflistungen in System.Collections.Concurrent (Teil von PFX) enthält .NET Framework 4 eine neue Gruppenauflistung in System.Collections.Generic, die als SortedSet<T> bezeichnet wird.
Genau wie HashSet<T>, das in .NET 3.5 SortedSet eingeführt wurde, handelt es sich bei SortedSet<T> um eine Auflistung eindeutiger Elemente, im Gegensatz zu HashSet<T> behält SortedSet<T> die Elemente in sortierter Reihenfolge bei.
SortedSet<T> wird mithilfe eines sich selbst ausgleichenden Rot-Schwarz-Baums implementiert, der eine Leistungskomplexität von O(log n) für Einfügen, Löschen und Suchen liefert.
HashSet<T> hingegen bietet eine etwas bessere Leistung von O(1) für Einfügen, Löschen und Suchen.
Wenn Sie nur eine Gruppe für allgemeine Zwecke benötigen, bietet sich für die meisten Fälle HashSet<T> an.
Wenn Sie jedoch die Elemente in sortierter Reihenfolge beibehalten, die Teilmenge der Elemente in einem bestimmten Bereich abrufen oder die min- bzw. max-Elemente abrufen möchten, sollten Sie SortedSet<T> verwenden.
Der folgende Code veranschaulicht die Verwendung von SortedSet<T> mit ganzen Zahlen:
var set1 = new SortedSet<int>() { 2, 5, 6, 2, 1, 4, 8 };
bool first = true;
foreach (var i in set1) {
if (first) {
first = false;
}
else {
Console.Write(",");
}
Console.Write(i);
}
// Output: 1,2,4,5,6,8
Die Gruppe wird erstellt und mit der Auflistungsinitialisierungssyntax von C# initialisiert.
Beachten Sie, dass die ganzen Zahlen der Gruppe in keiner bestimmten Reihenfolge hinzugefügt werden.
Beachten Sie, dass 2 zweimal hinzugefügt wird.
Es sollte nun keine Überraschung sein, wenn beim Durchlaufen der Elemente von Gruppe 1 festgestellt wird, dass die ganzen Zahlen in sortierter Reihenfolge sind und die Gruppe nur eine 2 enthält.
Genau wie HashSet<T> weist die Add-Methode von SortedSet<T> den Rückgabetyp "bool" auf, der verwendet werden kann, um zu bestimmen, ob das Element erfolgreich ("true") hinzugefügt wurde oder ob es nicht hinzugefügt wurde, weil die Gruppe bereits in Element enthält ("false").
Abbildung 3 veranschaulicht die max- und min-Elemente innerhalb der Gruppe und ruft eine Teilmenge von Elementen in einem bestimmten Bereich ab.
Abbildung 3 Abrufen von Max- bzw. Min-Elementen und Teilmengenansichten
var set1 = new SortedSet<int>() { 2, 5, 6, 2, 1, 4, 8 };
Console.WriteLine("Min: {0}", set1.Min);
Console.WriteLine("Max: {0}", set1.Max);
var subset1 = set1.GetViewBetween(2, 6);
Console.Write("Subset View: ");
bool first = true;
foreach (var i in subset1) {
if (first) {
first = false;
}
else {
Console.Write(",");
}
Console.Write(i);
}
// Output:
// Min: 1
// Max: 8
// Subset View: 2,4,5,6
Die GetViewBetween-Methode gibt einen Überblick über die ursprüngliche Gruppe zurück.
Dies bedeutet, dass Änderungen an der Ansicht auch in der ursprünglichen Gruppe reflektiert werden.
Wenn 3 im Code oben subset1 hinzugefügt wird, wird es tatsächlich set1 hinzugefügt.
Beachten Sie, dass Sie keine Elemente zu einer Ansicht außerhalb der angegebenen Begrenzungen hinzufügen können.
Wenn Sie beispielsweise versuchen, im Code oben eine 9 zu subset1 hinzuzufügen, tritt eine Argumentausnahme auf, da die Ansicht zwischen 2 und 6 ist.
Versuchen Sie es einmal.
Die in dieser Kolumne beschriebenen BCL-Funktionen sind ein Beispiel für die neuen in .NET Framework 4 verfügbaren Funktionen.
Diese Funktionen sind im Vorschauformular als Teil von .NET Framework 4 Beta 1 verfügbar, das zusammen mit Visual Studio 2010 Beta 1 unter msdn.microsoft.com/netframework/dd819232.aspx zum Download zur Verfügung steht.
Laden Sie die Betaversion herunter, testen Sie die neue Funktionalität, und teilen Sie uns Ihre Meinung mit unter connect.microsoft.com/VisualStudio/content/content.aspx?ContentID=12362.
Überprüfen Sie auch den BCL-Teamblog regelmäßig auf neue Beiträge zu einigen der weiteren BCL-Ergänzungen und eine Ankündigung zu den Neuigkeiten in Beta 2.
Justin Van Patten
ist ein Programmmanager im CLR-Team bei Microsoft, wo er sich mit Basisklassenbibliotheken befasst.
Sie können ihn über seinen BCL-Teamblog unter blogs.msdn.com/bclteam erreichen.
|