Share via


Gör så här: Använd LINQ för att fråga efter filer och kataloger

Många filsystemåtgärder är i huvudsak frågor och passar därför bra för LINQ-metoden. Dessa frågor är icke-förstörande. De ändrar inte innehållet i de ursprungliga filerna eller mapparna. Frågor bör inte orsaka några biverkningar. I allmänhet bör all kod (inklusive frågor som utför åtgärder för att skapa/uppdatera/ta bort) som ändrar källdata hållas åtskilda från koden som bara frågar efter data.

Det finns en viss komplexitet i att skapa en datakälla som korrekt representerar innehållet i filsystemet och hanterar undantag på ett korrekt sätt. Exemplen i det här avsnittet skapar en samling ögonblicksbilder av FileInfo objekt som representerar alla filer under en angiven rotmapp och alla dess undermappar. Det faktiska tillståndet för var FileInfo och en kan ändras i tiden mellan när du börjar och slutar köra en fråga. Du kan till exempel skapa en lista över FileInfo objekt som ska användas som datakälla. Om du försöker komma åt Length egenskapen i en fråga FileInfo försöker objektet komma åt filsystemet för att uppdatera värdet Lengthför . Om filen inte längre finns får du en FileNotFoundException i din fråga, även om du inte frågar filsystemet direkt.

Fråga efter filer med ett angivet attribut eller namn

Det här exemplet visar hur du hittar alla filer som har ett angivet filnamnstillägg (till exempel ".txt") i ett angivet katalogträd. Den visar också hur du returnerar antingen den nyaste eller äldsta filen i trädet baserat på skapandetiden. Du kan behöva ändra den första raden i många av exemplen oavsett om du kör den här koden på antingen Windows, Mac eller ett Linux-system.

string startFolder = """C:\Program Files\dotnet\sdk""";
// Or
// string startFolder = "/usr/local/share/dotnet/sdk";

DirectoryInfo dir = new DirectoryInfo(startFolder);
var fileList = dir.GetFiles("*.*", SearchOption.AllDirectories);

var fileQuery = from file in fileList
                where file.Extension == ".txt"
                orderby file.Name
                select file;

// Uncomment this block to see the full query
// foreach (FileInfo fi in fileQuery)
// {
//    Console.WriteLine(fi.FullName);
// }

var newestFile = (from file in fileQuery
                  orderby file.CreationTime
                  select new { file.FullName, file.CreationTime })
                  .Last();

Console.WriteLine($"\r\nThe newest .txt file is {newestFile.FullName}. Creation time: {newestFile.CreationTime}");

Gruppera filer efter tillägg

Det här exemplet visar hur LINQ kan användas för att utföra avancerade grupperings- och sorteringsåtgärder på listor över filer eller mappar. Den visar också hur du sidutdata i konsolfönstret med hjälp Skip av metoderna och Take .

Följande fråga visar hur du grupperar innehållet i ett angivet katalogträd efter filnamnstillägget.

string startFolder = """C:\Program Files\dotnet\sdk""";
// Or
// string startFolder = "/usr/local/share/dotnet/sdk";

int trimLength = startFolder.Length;

DirectoryInfo dir = new DirectoryInfo(startFolder);

var fileList = dir.GetFiles("*.*", SearchOption.AllDirectories);

var queryGroupByExt = from file in fileList
                      group file by file.Extension.ToLower() into fileGroup
                      orderby fileGroup.Count(), fileGroup.Key
                      select fileGroup;

// Iterate through the outer collection of groups.
foreach (var filegroup in queryGroupByExt.Take(5))
{
    Console.WriteLine($"Extension: {filegroup.Key}");
    var resultPage = filegroup.Take(20);

    //Execute the resultPage query
    foreach (var f in resultPage)
    {
        Console.WriteLine($"\t{f.FullName.Substring(trimLength)}");
    }
    Console.WriteLine();
}

Utdata från det här programmet kan vara långa, beroende på information om det lokala filsystemet och vad som startFolder är inställt på. Om du vill aktivera visning av alla resultat visar det här exemplet hur du bläddrar igenom resultat. En kapslad foreach loop krävs eftersom varje grupp räknas upp separat.

Så här frågar du efter det totala antalet byte i en uppsättning mappar

Det här exemplet visar hur du hämtar det totala antalet byte som används av alla filer i en angiven mapp och alla dess undermappar. Metoden Sum lägger till värdena för alla objekt som valts select i -satsen. Du kan ändra den här frågan för att hämta den största eller minsta filen i det angivna katalogträdet genom att anropa Min metoden eller Max i stället för Sum.

string startFolder = """C:\Program Files\dotnet\sdk""";
// Or
// string startFolder = "/usr/local/share/dotnet/sdk";

var fileList = Directory.GetFiles(startFolder, "*.*", SearchOption.AllDirectories);

var fileQuery = from file in fileList
                let fileLen = new FileInfo(file).Length
                where fileLen > 0
                select fileLen;

// Cache the results to avoid multiple trips to the file system.
long[] fileLengths = fileQuery.ToArray();

// Return the size of the largest file
long largestFile = fileLengths.Max();

// Return the total number of bytes in all the files under the specified folder.
long totalBytes = fileLengths.Sum();

Console.WriteLine($"There are {totalBytes} bytes in {fileList.Count()} files under {startFolder}");
Console.WriteLine($"The largest file is {largestFile} bytes.");

I det här exemplet utökas föregående exempel för att göra följande:

  • Så här hämtar du storleken i byte för den största filen.
  • Så här hämtar du storleken i byte för den minsta filen.
  • Hämta objektets FileInfo största eller minsta fil från en eller flera mappar under en angiven rotmapp.
  • Så här hämtar du en sekvens, till exempel de 10 största filerna.
  • Så här beställer du filer i grupper baserat på deras filstorlek i byte och ignorerar filer som är mindre än en angiven storlek.

Följande exempel innehåller fem separata frågor som visar hur du frågar och grupperar filer, beroende på filstorleken i byte. Du kan ändra dessa exempel för att basera frågan på någon annan egenskap för FileInfo objektet.

// Return the FileInfo object for the largest file
// by sorting and selecting from beginning of list
FileInfo longestFile = (from file in fileList
                        let fileInfo = new FileInfo(file)
                        where fileInfo.Length > 0
                        orderby fileInfo.Length descending
                        select fileInfo
                        ).First();

Console.WriteLine($"The largest file under {startFolder} is {longestFile.FullName} with a length of {longestFile.Length} bytes");

//Return the FileInfo of the smallest file
FileInfo smallestFile = (from file in fileList
                         let fileInfo = new FileInfo(file)
                         where fileInfo.Length > 0
                         orderby fileInfo.Length ascending
                         select fileInfo
                        ).First();

Console.WriteLine($"The smallest file under {startFolder} is {smallestFile.FullName} with a length of {smallestFile.Length} bytes");

//Return the FileInfos for the 10 largest files
var queryTenLargest = (from file in fileList
                       let fileInfo = new FileInfo(file)
                       let len = fileInfo.Length
                       orderby len descending
                       select fileInfo
                      ).Take(10);

Console.WriteLine($"The 10 largest files under {startFolder} are:");

foreach (var v in queryTenLargest)
{
    Console.WriteLine($"{v.FullName}: {v.Length} bytes");
}

// Group the files according to their size, leaving out
// files that are less than 200000 bytes.
var querySizeGroups = from file in fileList
                      let fileInfo = new FileInfo(file)
                      let len = fileInfo.Length
                      where len > 0
                      group fileInfo by (len / 100000) into fileGroup
                      where fileGroup.Key >= 2
                      orderby fileGroup.Key descending
                      select fileGroup;

foreach (var filegroup in querySizeGroups)
{
    Console.WriteLine($"{filegroup.Key}00000");
    foreach (var item in filegroup)
    {
        Console.WriteLine($"\t{item.Name}: {item.Length}");
    }
}

Om du vill returnera ett eller flera fullständiga FileInfo objekt måste frågan först undersöka var och en i datakällan och sedan sortera dem efter värdet för egenskapen Längd. Sedan kan den returnera den enda eller sekvensen med de största längderna. Använd First för att returnera det första elementet i en lista. Använd Take för att returnera det första n antalet element. Ange en fallande sorteringsordning för att placera de minsta elementen i början av listan.

Så här frågar du efter dubblettfiler i ett katalogträd

Ibland kan filer som har samma namn finnas i mer än en mapp. Det här exemplet visar hur du frågar efter sådana duplicerade filnamn under en angiven rotmapp. Det andra exemplet visar hur du frågar efter filer vars storlek och LastWrite-tider också matchar.

string startFolder = """C:\Program Files\dotnet\sdk""";
// Or
// string startFolder = "/usr/local/share/dotnet/sdk";

DirectoryInfo dir = new DirectoryInfo(startFolder);

IEnumerable<FileInfo> fileList = dir.GetFiles("*.*", SearchOption.AllDirectories);

// used in WriteLine to keep the lines shorter
int charsToSkip = startFolder.Length;

// var can be used for convenience with groups.
var queryDupNames = from file in fileList
                    group file.FullName.Substring(charsToSkip) by file.Name into fileGroup
                    where fileGroup.Count() > 1
                    select fileGroup;

foreach (var queryDup in queryDupNames.Take(20))
{
    Console.WriteLine($"Filename = {(queryDup.Key.ToString() == string.Empty ? "[none]" : queryDup.Key.ToString())}");

    foreach (var fileName in queryDup.Take(10))
    {
        Console.WriteLine($"\t{fileName}");
    }   
}

Den första frågan använder en nyckel för att fastställa en matchning. Den hittar filer som har samma namn men vars innehåll kan vara annorlunda. Den andra frågan använder en sammansatt nyckel för att matcha mot objektets FileInfo tre egenskaper. Den här frågan är mycket mer sannolikt att hitta filer som har samma namn och liknande eller identiskt innehåll.

    string startFolder = """C:\Program Files\dotnet\sdk""";
    // Or
    // string startFolder = "/usr/local/share/dotnet/sdk";

    // Make the lines shorter for the console display
    int charsToSkip = startFolder.Length;

    // Take a snapshot of the file system.
    DirectoryInfo dir = new DirectoryInfo(startFolder);
    IEnumerable<FileInfo> fileList = dir.GetFiles("*.*", SearchOption.AllDirectories);

    // Note the use of a compound key. Files that match
    // all three properties belong to the same group.
    // A named type is used to enable the query to be
    // passed to another method. Anonymous types can also be used
    // for composite keys but cannot be passed across method boundaries
    //
    var queryDupFiles = from file in fileList
                        group file.FullName.Substring(charsToSkip) by
                        (Name: file.Name, LastWriteTime: file.LastWriteTime, Length: file.Length )
                        into fileGroup
                        where fileGroup.Count() > 1
                        select fileGroup;

    foreach (var queryDup in queryDupFiles.Take(20))
    {
        Console.WriteLine($"Filename = {(queryDup.Key.ToString() == string.Empty ? "[none]" : queryDup.Key.ToString())}");

        foreach (var fileName in queryDup)
        {
            Console.WriteLine($"\t{fileName}");
        }
    }
}

Så här frågar du innehållet i textfiler i en mapp

Det här exemplet visar hur du frågar efter alla filer i ett angivet katalogträd, öppnar varje fil och inspekterar dess innehåll. Den här typen av teknik kan användas för att skapa index eller omvända index för innehållet i ett katalogträd. En enkel strängsökning utförs i det här exemplet. Mer komplexa typer av mönstermatchning kan dock utföras med ett reguljärt uttryck.

string startFolder = """C:\Program Files\dotnet\sdk""";
// Or
// string startFolder = "/usr/local/share/dotnet/sdk";

DirectoryInfo dir = new DirectoryInfo(startFolder);

var fileList = dir.GetFiles("*.*", SearchOption.AllDirectories);

string searchTerm = "change";

var queryMatchingFiles = from file in fileList
                         where file.Extension == ".txt"
                         let fileText = File.ReadAllText(file.FullName)
                         where fileText.Contains(searchTerm)
                         select file.FullName;

// Execute the query.
Console.WriteLine($"""The term "{searchTerm}" was found in:""");
foreach (string filename in queryMatchingFiles)
{
    Console.WriteLine(filename);
}

Så här jämför du innehållet i två mappar

Det här exemplet visar tre sätt att jämföra två fillistor:

  • Genom att fråga efter ett booleskt värde som anger om de två fillistorna är identiska.
  • Genom att fråga efter skärningspunkten för att hämta filerna som finns i båda mapparna.
  • Genom att fråga efter den angivna skillnaden för att hämta filerna som finns i en mapp men inte den andra.

De tekniker som visas här kan anpassas för att jämföra sekvenser av objekt av vilken typ som helst.

Klassen FileComparer som visas här visar hur du använder en anpassad jämförelseklass tillsammans med Standard Query Operators. Klassen är inte avsedd för användning i verkliga scenarier. Den använder bara namn och längd i byte för varje fil för att avgöra om innehållet i varje mapp är identiskt eller inte. I ett verkligt scenario bör du ändra den här jämförelsen för att utföra en mer rigorös likhetskontroll.

// This implementation defines a very simple comparison
// between two FileInfo objects. It only compares the name
// of the files being compared and their length in bytes.
class FileCompare : IEqualityComparer<FileInfo>
{
    public bool Equals(FileInfo? f1, FileInfo? f2)
    {
        return (f1?.Name == f2?.Name &&
                f1?.Length == f2?.Length);
    }

    // Return a hash that reflects the comparison criteria. According to the
    // rules for IEqualityComparer<T>, if Equals is true, then the hash codes must
    // also be equal. Because equality as defined here is a simple value equality, not
    // reference identity, it is possible that two or more objects will produce the same
    // hash code.
    public int GetHashCode(FileInfo fi)
    {
        string s = $"{fi.Name}{fi.Length}";
        return s.GetHashCode();
    }
}

public static void CompareDirectories()
{
    string pathA = """C:\Program Files\dotnet\sdk\8.0.104""";
    string pathB = """C:\Program Files\dotnet\sdk\8.0.204""";

    DirectoryInfo dir1 = new DirectoryInfo(pathA);
    DirectoryInfo dir2 = new DirectoryInfo(pathB);

    IEnumerable<FileInfo> list1 = dir1.GetFiles("*.*", SearchOption.AllDirectories);
    IEnumerable<FileInfo> list2 = dir2.GetFiles("*.*", SearchOption.AllDirectories);

    //A custom file comparer defined below
    FileCompare myFileCompare = new FileCompare();

    // This query determines whether the two folders contain
    // identical file lists, based on the custom file comparer
    // that is defined in the FileCompare class.
    // The query executes immediately because it returns a bool.
    bool areIdentical = list1.SequenceEqual(list2, myFileCompare);

    if (areIdentical == true)
    {
        Console.WriteLine("the two folders are the same");
    }
    else
    {
        Console.WriteLine("The two folders are not the same");
    }

    // Find the common files. It produces a sequence and doesn't
    // execute until the foreach statement.
    var queryCommonFiles = list1.Intersect(list2, myFileCompare);

    if (queryCommonFiles.Any())
    {
        Console.WriteLine($"The following files are in both folders (total number = {queryCommonFiles.Count()}):");
        foreach (var v in queryCommonFiles.Take(10))
        {
            Console.WriteLine(v.Name); //shows which items end up in result list
        }
    }
    else
    {
        Console.WriteLine("There are no common files in the two folders.");
    }

    // Find the set difference between the two folders.
    var queryList1Only = (from file in list1
                          select file)
                          .Except(list2, myFileCompare);

    Console.WriteLine();
    Console.WriteLine($"The following files are in list1 but not list2 (total number = {queryList1Only.Count()}):");
    foreach (var v in queryList1Only.Take(10))
    {
        Console.WriteLine(v.FullName);
    }

    var queryList2Only = (from file in list2
                          select file)
                          .Except(list1, myFileCompare);

    Console.WriteLine();
    Console.WriteLine($"The following files are in list2 but not list1 (total number = {queryList2Only.Count()}:");
    foreach (var v in queryList2Only.Take(10))
    {
        Console.WriteLine(v.FullName);
    }
}

Så här ändrar du ordning på fälten i en avgränsad fil

En CSV-fil (kommaavgränsat värde) är en textfil som ofta används för att lagra kalkylbladsdata eller andra tabelldata som representeras av rader och kolumner. Genom att använda Split metoden för att separera fälten är det enkelt att fråga efter och ändra CSV-filer med LINQ. I själva verket kan samma teknik användas för att ordna om delarna i en strukturerad textrad. det är inte begränsat till CSV-filer.

I följande exempel antar du att de tre kolumnerna representerar elevernas "familjenamn", "förnamn" och "ID". Fälten är i alfabetisk ordning baserat på elevernas familjenamn. Frågan skapar en ny sekvens där ID-kolumnen visas först, följt av en andra kolumn som kombinerar elevens förnamn och efternamn. Raderna sorteras om enligt ID-fältet. Resultatet sparas i en ny fil och ursprungliga data ändras inte. Följande text visar innehållet i den spreadsheet1.csv fil som används i följande exempel:

Adams,Terry,120
Fakhouri,Fadi,116
Feng,Hanying,117
Garcia,Cesar,114
Garcia,Debra,115
Garcia,Hugo,118
Mortensen,Sven,113
O'Donnell,Claire,112
Omelchenko,Svetlana,111
Tucker,Lance,119
Tucker,Michael,122
Zabokritski,Eugene,121

Följande kod läser källfilen och ordnar om varje kolumn i CSV-filen för att ordna om ordningen på kolumnerna:

string[] lines = File.ReadAllLines("spreadsheet1.csv");

// Create the query. Put field 2 first, then
// reverse and combine fields 0 and 1 from the old field
IEnumerable<string> query = from line in lines
                            let fields = line.Split(',')
                            orderby fields[2]
                            select $"{fields[2]}, {fields[1]} {fields[0]}";

File.WriteAllLines("spreadsheet2.csv", query.ToArray());

/* Output to spreadsheet2.csv:
111, Svetlana Omelchenko
112, Claire O'Donnell
113, Sven Mortensen
114, Cesar Garcia
115, Debra Garcia
116, Fadi Fakhouri
117, Hanying Feng
118, Hugo Garcia
119, Lance Tucker
120, Terry Adams
121, Eugene Zabokritski
122, Michael Tucker
*/

Dela upp en fil i många filer med hjälp av grupper

Det här exemplet visar ett sätt att sammanfoga innehållet i två filer och sedan skapa en uppsättning nya filer som organiserar data på ett nytt sätt. Frågan använder innehållet i två filer. Följande text visar innehållet i den första filen, names1.txt:

Bankov, Peter
Holm, Michael
Garcia, Hugo
Potra, Cristina
Noriega, Fabricio
Aw, Kam Foo
Beebe, Ann
Toyoshima, Tim
Guy, Wey Yuan
Garcia, Debra

Den andra filen, names2.txt, innehåller en annan uppsättning namn, varav några är gemensamma med den första uppsättningen:

Liu, Jinghao
Bankov, Peter
Holm, Michael
Garcia, Hugo
Beebe, Ann
Gilchrist, Beth
Myrcha, Jacek
Giakoumakis, Leo
McLin, Nkenge
El Yassir, Mehdi

Följande kod frågar båda filerna, tar union av båda filerna och skriver sedan en ny fil för varje grupp, definierad av den första bokstaven i familjenamnet:

string[] fileA = File.ReadAllLines("names1.txt");
string[] fileB = File.ReadAllLines("names2.txt");

// Concatenate and remove duplicate names
var mergeQuery = fileA.Union(fileB);

// Group the names by the first letter in the last name.
var groupQuery = from name in mergeQuery
                 let n = name.Split(',')[0]
                 group name by n[0] into g
                 orderby g.Key
                 select g;

foreach (var g in groupQuery)
{
    string fileName = $"testFile_{g.Key}.txt";

    Console.WriteLine(g.Key);

    using StreamWriter sw = new StreamWriter(fileName);
    foreach (var item in g)
    {
        sw.WriteLine(item);
        // Output to console for example purposes.
        Console.WriteLine($"   {item}");
    }
}
/* Output:
    A
       Aw, Kam Foo
    B
       Bankov, Peter
       Beebe, Ann
    E
       El Yassir, Mehdi
    G
       Garcia, Hugo
       Guy, Wey Yuan
       Garcia, Debra
       Gilchrist, Beth
       Giakoumakis, Leo
    H
       Holm, Michael
    L
       Liu, Jinghao
    M
       Myrcha, Jacek
       McLin, Nkenge
    N
       Noriega, Fabricio
    P
       Potra, Cristina
    T
       Toyoshima, Tim
 */

Så här ansluter du innehåll från olika filer

Det här exemplet visar hur du kopplar data från två kommaavgränsade filer som delar ett gemensamt värde som används som matchande nyckel. Den här tekniken kan vara användbar om du måste kombinera data från två kalkylblad, eller från ett kalkylblad och från en fil som har ett annat format, till en ny fil. Du kan ändra exemplet så att det fungerar med alla typer av strukturerad text.

Följande text visar innehållet i scores.csv. Filen representerar kalkylbladsdata. Kolumn 1 är elevens ID och kolumnerna 2 till och med 5 är testresultat.

111, 97, 92, 81, 60
112, 75, 84, 91, 39
113, 88, 94, 65, 91
114, 97, 89, 85, 82
115, 35, 72, 91, 70
116, 99, 86, 90, 94
117, 93, 92, 80, 87
118, 92, 90, 83, 78
119, 68, 79, 88, 92
120, 99, 82, 81, 79
121, 96, 85, 91, 60
122, 94, 92, 91, 91

Följande text visar innehållet i names.csv. Filen representerar ett kalkylblad som innehåller elevens familjenamn, förnamn och elev-ID.

Omelchenko,Svetlana,111
O'Donnell,Claire,112
Mortensen,Sven,113
Garcia,Cesar,114
Garcia,Debra,115
Fakhouri,Fadi,116
Feng,Hanying,117
Garcia,Hugo,118
Tucker,Lance,119
Adams,Terry,120
Zabokritski,Eugene,121
Tucker,Michael,122

Koppla innehåll från olika filer som innehåller relaterad information. Filen names.csv innehåller elevnamnet plus ett ID-nummer. Filen scores.csv innehåller ID:t och en uppsättning med fyra testresultat. Följande fråga kopplar poängen till elevnamnen med hjälp av ID som matchande nyckel. Koden visas i följande exempel:

string[] names = File.ReadAllLines(@"names.csv");
string[] scores = File.ReadAllLines(@"scores.csv");

var scoreQuery = from name in names
                  let nameFields = name.Split(',')
                  from id in scores
                  let scoreFields = id.Split(',')
                  where Convert.ToInt32(nameFields[2]) == Convert.ToInt32(scoreFields[0])
                  select $"{nameFields[0]},{scoreFields[1]},{scoreFields[2]},{scoreFields[3]},{scoreFields[4]}";

Console.WriteLine("\r\nMerge two spreadsheets:");
foreach (string item in scoreQuery)
{
    Console.WriteLine(item);
}
Console.WriteLine("{0} total names in list", scoreQuery.Count());
/* Output:
Merge two spreadsheets:
Omelchenko, 97, 92, 81, 60
O'Donnell, 75, 84, 91, 39
Mortensen, 88, 94, 65, 91
Garcia, 97, 89, 85, 82
Garcia, 35, 72, 91, 70
Fakhouri, 99, 86, 90, 94
Feng, 93, 92, 80, 87
Garcia, 92, 90, 83, 78
Tucker, 68, 79, 88, 92
Adams, 99, 82, 81, 79
Zabokritski, 96, 85, 91, 60
Tucker, 94, 92, 91, 91
12 total names in list
 */

Så här beräknar du kolumnvärden i en CSV-textfil

Det här exemplet visar hur du utför aggregerade beräkningar som Sum, Average, Min och Max på kolumnerna i en .csv fil. Exempelprinciperna som visas här kan tillämpas på andra typer av strukturerad text.

Följande text visar innehållet i scores.csv. Anta att den första kolumnen representerar ett student-ID och efterföljande kolumner representerar poäng från fyra prov.

111, 97, 92, 81, 60
112, 75, 84, 91, 39
113, 88, 94, 65, 91
114, 97, 89, 85, 82
115, 35, 72, 91, 70
116, 99, 86, 90, 94
117, 93, 92, 80, 87
118, 92, 90, 83, 78
119, 68, 79, 88, 92
120, 99, 82, 81, 79
121, 96, 85, 91, 60
122, 94, 92, 91, 91

Följande text visar hur du använder Split metoden för att konvertera varje textrad till en matris. Varje matriselement representerar en kolumn. Slutligen konverteras texten i varje kolumn till dess numeriska representation.

public class SumColumns
{
    public static void SumCSVColumns(string fileName)
    {
        string[] lines = File.ReadAllLines(fileName);

        // Specifies the column to compute.
        int exam = 3;

        // Spreadsheet format:
        // Student ID    Exam#1  Exam#2  Exam#3  Exam#4
        // 111,          97,     92,     81,     60

        // Add one to exam to skip over the first column,
        // which holds the student ID.
        SingleColumn(lines, exam + 1);
        Console.WriteLine();
        MultiColumns(lines);
    }

    static void SingleColumn(IEnumerable<string> strs, int examNum)
    {
        Console.WriteLine("Single Column Query:");

        // Parameter examNum specifies the column to
        // run the calculations on. This value could be
        // passed in dynamically at run time.

        // Variable columnQuery is an IEnumerable<int>.
        // The following query performs two steps:
        // 1) use Split to break each row (a string) into an array
        //    of strings,
        // 2) convert the element at position examNum to an int
        //    and select it.
        var columnQuery = from line in strs
                          let elements = line.Split(',')
                          select Convert.ToInt32(elements[examNum]);

        // Execute the query and cache the results to improve
        // performance. This is helpful only with very large files.
        var results = columnQuery.ToList();

        // Perform aggregate calculations Average, Max, and
        // Min on the column specified by examNum.
        double average = results.Average();
        int max = results.Max();
        int min = results.Min();

        Console.WriteLine($"Exam #{examNum}: Average:{average:##.##} High Score:{max} Low Score:{min}");
    }

    static void MultiColumns(IEnumerable<string> strs)
    {
        Console.WriteLine("Multi Column Query:");

        // Create a query, multiColQuery. Explicit typing is used
        // to make clear that, when executed, multiColQuery produces
        // nested sequences. However, you get the same results by
        // using 'var'.

        // The multiColQuery query performs the following steps:
        // 1) use Split to break each row (a string) into an array
        //    of strings,
        // 2) use Skip to skip the "Student ID" column, and store the
        //    rest of the row in scores.
        // 3) convert each score in the current row from a string to
        //    an int, and select that entire sequence as one row
        //    in the results.
        var multiColQuery = from line in strs
                            let elements = line.Split(',')
                            let scores = elements.Skip(1)
                            select (from str in scores
                                    select Convert.ToInt32(str));

        // Execute the query and cache the results to improve
        // performance.
        // ToArray could be used instead of ToList.
        var results = multiColQuery.ToList();

        // Find out how many columns you have in results.
        int columnCount = results[0].Count();

        // Perform aggregate calculations Average, Max, and
        // Min on each column.
        // Perform one iteration of the loop for each column
        // of scores.
        // You can use a for loop instead of a foreach loop
        // because you already executed the multiColQuery
        // query by calling ToList.
        for (int column = 0; column < columnCount; column++)
        {
            var results2 = from row in results
                           select row.ElementAt(column);
            double average = results2.Average();
            int max = results2.Max();
            int min = results2.Min();

            // Add one to column because the first exam is Exam #1,
            // not Exam #0.
            Console.WriteLine($"Exam #{column + 1} Average: {average:##.##} High Score: {max} Low Score: {min}");
        }
    }
}
/* Output:
    Single Column Query:
    Exam #4: Average:76.92 High Score:94 Low Score:39

    Multi Column Query:
    Exam #1 Average: 86.08 High Score: 99 Low Score: 35
    Exam #2 Average: 86.42 High Score: 94 Low Score: 72
    Exam #3 Average: 84.75 High Score: 91 Low Score: 65
    Exam #4 Average: 76.92 High Score: 94 Low Score: 39
 */

Om filen är en flikavgränsad fil uppdaterar du bara argumentet i Split -metoden till \t.