Lesson # 14. Working with Files. Reading from a file

Programming on c sharp in Microsoft Visual Studio. Working with Files. Reading from a file

Lesson # 14. Theory

Lecture in pdf format

Files classification

  • Component type:
  • text files (consists of strings in some encoding)
  • binary files
  • Access type:
  • Arbitrary: access time = Θ(1), components have the same size
    (bytes in the simplest case)
  • Sequential: components have different sizes, for seeking to the i-th component it’s necessary to pass the preceding i-1 components (example: text files)
  • Operations type:
  • reading only
  • writing only
  • reading and writing

Operations with files

With closed files:
open
check existence
delete
copy
rename
move
With already opened files:
close
write info
read info
check the end of file (EOF)
seek to the n-th position

Main .NET classes & namespaces

    Namespaces:

  • using System.IO;
  • using System.Text;
  • In .net we have a namespace called System.IO, that’s where a lot of classes to work with files and directories are located.

    Classes:

  • File – static class, operations with closed files
  • FileMode – mode of file opening
  • FileStream
  • StreamReader/StreamWriter – streams for reading-writing to text files
  • BinaryReader/BinaryWriter — streams for reading-writing to binary files
  • Encoding – file encodings
  • File and FileInfo classes both provide methods for creating, copying, deleting, moving and opening files. They have very similar interfaces, the only difference is FileInfo provides instance methods, whereas File provides static methods.
  • The difference is if you’re gonna have a small number of operations, it’s more convenient to access the static methods of the File class. If you’re going to have many operations, it’s more efficient to create a FileInfo class and access all its instance methods.
  • Directory class provides static methods, whereas DirectoryInfo provides instance methods.
  • We also have the Path class which provides methods to work with a string that contains a file or directory path information.
FileMode enum type
  • FileMode.Create
  • FileMode.Open
  • FileMode.OpenOrCreate
  • FileMode.Append
  • Main methods of File class

      Operations with closed files:

      using System.IO;
       
      // File.Exists("a.dat")
      Console.WriteLine(File.Exists("a.dat")); // true or false
      // File.Exists("d:\\a.dat")
      Console.WriteLine(File.Exists("d:\\a.dat")); // true or false
      // File.Exists(@"d:\a.dat")
      Console.WriteLine(File.Exists(@"d:\a.dat"); // true or false
       
      File.Delete("a.dat");
      File.Copy("d:\\a.dat", "d:\\b.dat"); // b-file will be created, if it exists - the exception will occur
      File.Copy("a.dat", @"d:\a.dat");
      File.Move("a.dat", @"..\a.dat");

    try .. catch statement

    try
    {
      // code with possible exception
    }
    catch (Exception e)
    {
      // to handle of an exception
    }

    Example:

    try
      {
        var path = @"d:\a.dat";
        var fs = new FileStream(path, FileMode.Open);
        ...
      }
    catch (FileNotFoundException e)
      {
        Console.WriteLine($"File with a name {e.FileName} is not found");
      }
    catch (FileLoadException e)
      {
         Console.WriteLine($"File with a name {e.FileName} can't be accessed");
      }

    Using statement

    Example of working with binary files:

    using System.IO;
    // ...
     
    string path = @"Lesson14.dat";
    // data within the file: 1 2 3 4 
    var fs = new FileStream(path, FileMode.Open);
    var br = new BinaryReader(fs);
    var a=br.ReadInt32();
    Console.WriteLine(a); // 1
    var b = br.ReadInt32(); 
    Console.WriteLine(b); // 2
    var c = br.ReadInt32();
    Console.WriteLine(c); // 3
     
    fs.Close();
    • Using statement provides an automatic call of Close() method while working with FileStream class.

    Example:

    var path = @"d:\a.dat";
    using (var fs = new FileStream(path, FileMode.Create))
    {
      fs.WriteByte(1);
      fs.WriteByte(2);
      fs.WriteByte(3);
    }
    using (var fs = new FileStream(path, FileMode.Open))
    {
      Console.WriteLine(fs.ReadByte());
      Console.WriteLine(fs.ReadByte());
      Console.WriteLine(fs.ReadByte());
    }

    Labs and Tasks

    Stream adapters for binary files

    Handling errors while working with files. Using statement. Reading data from a file

    Lab 1. Error of reading access to a non-existent file. Reading data from a file
      
    To do:

    • Create a program to output the contents of the file to the console window (the numbers can be outputted separated by whitespaces within a single line). You should use BinaryReader class.
    • Download the file L01.dat and place it into your work folder (~/Lesson_14Lab1/bin/Debug). There are 15 integer numbers written in the file.
    • You should use the using statement while working with an already opened file. Then, regardless of the success of working with an open file, it will always be closed.
    • using (var fs = new FileStream(path, FileMode.Create))
      {
        ...
      }
    • Develop a program with exception handling:
      • To prevent the program from «crashing» and not scare the user, add exception handling — the try..catch block. The using statement must be inside the try..catch block.
      • ...
        try
        {
             ...
        }
        catch (IOException e)
        { 
              Console.WriteLine($"Error to read from the file: {e.Message}");
        }
    • Change the name of the file within the program code. Run the application and check the output. Change the name again to the previous one.
    • It isn’t necessary to create methods here.

    Expected output:

    The data from the file:
    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
    
    +++++++++++++
    The data from the file:
    error to read from file: Файл 'C:\Users\User\Documents\Visual Studio 2015\Projects\L14_Lab1\bin\Debug\L02.dat' не найден
    

     
    [Solution and Project name: Lesson_14Lab1, file name L14Lab1.cs]

    ✍ How to do:

    1. Create a new project with the name and file name as it is specified in the task.
    2. Download the file L01.dat and place it into your work folder (~/Lesson_14Lab1/bin/Debug).
    3. To avoid always printing the class Console, we can include its declaration right at the beginning of the editor window:
    4. ...
      using static System.Console;
      ...
      
    5. Below this code, include namespace System.IO for working with files:
    6. using System.IO;
      ...
      
    7. Within the Main function, declare a variable for storing the path to the file. It is better to use verbatim string (@ symbol):
    8. string path = @"L01.dat";
      
    9. To read something from the file, you first need to open the file. To open the file, it is required to specify the path to the file in your system and the mode — how the operating system should open a file.
    10. So we need to initialize a new instance of the FileStream class with the specified path and mode. The enum FileMode specifies how the operating system should open a file. We need to use Open mode, because we don’t need to write anything to the file. The variable fs will store a stream input:
    11. using (var fs = new FileStream(path, FileMode.Open))
      
      FileStream Class provides a Stream (a sequence of bytes) for/from a file, supporting both synchronous and asynchronous read and write operations.
    12. After that, we need to create a variable (br) as an instance of the BinaryReader class, to have the possibility to read data from the file as binary values:
    13. ...
      using (var br = new BinaryReader(fs))
      
    14. In order not to have the error if the file does not exist or if the name of the file is misspelled we will use try...catch block. Cut out the using statements and paste them inside the try block:
    15. ...
      try
                {
                      using (var fs = new FileStream(path, FileMode.Open))
                      using (var br = new BinaryReader(fs))
                }
      
    16. Besides, inside try block, we’re going to read the bytes or characters from the file and print them out. For such a purpose, the PeekChar method of BinaryReader class is used:
    17. // within the try block, after using statements:
      while (br.PeekChar() != -1)
             {
                 var x = br.ReadInt32();
                 Write($"{x} ");
              }
      
      BinaryReader class reads primitive data types as binary values in a specific encoding.
      PeekChar method returns the next available character, or -1 if no more characters are available or the stream does not support seeking.
    18. Within the catch block, we’re going to print out a message in a case when some error occurs, e.g. in the case when the file name is incorrect:
    19. // under try block:
      catch (IOException e)
                      {
                          WriteLine($"Error to read from file: {e.Message}");
                      }
      
      Class IOException stores the exception that is thrown when an I/O error occurs.
      Message is an attribute of the Exception class that returns the error message that explains the reason for the exception, or an empty string («»).
    20. At the end of the program code, place the ReadKey method in order not to close the console window while running the program in a debug mode:
    21. // after the closing brace of the catch block:
      ReadKey();
      
      ReadKey method obtains the next character or function key pressed by the user. The pressed key is displayed in the console window.
    22. Press F5 to run the program in a debugging mode and check the output.
    23. To see the result when the error occurs, change the file name, e.g.:
    24. string path = @"L02.dat";
      
    25. Press F5 to run the program in a debugging mode and check the output.
    26. Return the correct name to the file.
    27. Add comments with the text of the task and save the project. Download file .cs to the moodle system.

    Task 1:

    To do: Copy the file L01.dat from the previous Lab and paste it into the directory of this task (~/Lesson_14Task1/bin/Debug).

    1. Create a program to print out the squared numbers from the file to the console window (the numbers can be outputted separated by whitespace within a single line), and also calculate the number of even digits among the numbers. You should use the BinaryReader class. Make a program using try..catch block.

    Note: It isn’t necessary to create methods here.
         

    Expected output:

    Squared numbers:
    1 4 9 16 25 36 49 64 81 100 121 144 169 196 225
    The quantity of even numbers: 7
    +++++++++++++++
    Error to read from file: Файл 'c:\users\user\documents\visual studio 2015\Projects\L_14Task1\bin\Debug\L02.dat' не найден.
    

    [Solution and Project name: Lesson_14Task1, file name L14Task1.cs]

    Lab 2. Several try..catch blocks. Reading data from a file
      
    To do:

    • Download the file L01.dat. There are integer numbers written in the file. Print these numbers to the console window by N numbers in each line (N is entered (N > 0) and has a default value = 10). You must use BinaryReader class. For an empty file, output the «empty file» message.
    • There can be multiple try..catch blocks and each of them can handle different errors. Write your code so that one try..catch block handles errors that occur when opening a file, and another block handles errors that occur when working with an already opened file.
      • Then the program structure should be as follows:
      • ...
        try 
        {
            using ( ... )
            using ( ... )
            { 
                try 
                {
                    // work with opened file...
                }
                catch 
                {     
                    // handling an error of working with already opened file
                }
           }
        catch 
        {
            // handling an error of opening file
        }
    • Create a method with two parameters to do the task. The signature of the method:
    • static void PrintFileInRows(string path, int N=10)
    • Check to see if the try..catch blocks work properly. Modulate an error while working with already opened file, and error with a name of the file. After checking, return the previous (right) code, and make the comments of the wrong code.

    Expected output:

    Please input N:
    3
    1 2 3
    4 5 6
    7 8 9
    10 11 12
    13 14 15
    +++++++++++++
    Please input N:
    3
    Error to read from the file: Файл 'c:\users\user\documents\visual studio 2015\Projects\L_14Lab2\bin\Debug\L02.dat' не найден.
    +++++++++++++
    Please input N:
    3
    1 2 3
    4 5 6
    7 8 9
    10 11 12
    13 14 15 
    Error working with file: Чтение после конца потока невозможно.
    

     
    [Solution and Project name: Lesson_14Lab2, file name L14Lab2.cs]

    ✍ Algorithm:

    1. Create a new project with the name and file name as it is specified in the task.
    2. Download the file L01.dat and place it into your work folder (~/Lesson_14Lab2/bin/Debug).
    3. To avoid always printing the class Console, we can include its declaration right at the beginning of the editor window:
    4. ...
      using static System.Console;
      ...
      
    5. Below this code, include namespace System.IO for working with files:
    6. using System.IO;
      ...
      
    7. Within the Main function, declare a variable for storing the path to the file. It is better to use verbatim string (@ symbol):
    8. string path = @"L01.dat";
      
    9. We’re going to output the numbers from the file within some lines: N numbers in each line. So, within the Main function, we need to ask the user to enter N:
    10. ...
      WriteLine($"Please input N:");
      var n = int.Parse(ReadLine());
      
    11. All the rest what we’re going to do with the file we have to place inside a method. So we must declare a method with the name PrintFileInRows with two parameters — the path to the file and the number of the digits to output within each line (N):
    12. // after the closing brace of the Main function
      static void PrintFileInRows(string path, int n = 10)
            {
             ...
            }
      
      The method will not return anything, that is why we use void keyword.
    13. To read something from the file, you first need to open the file. But according to the requirement of the task, we must have two try..catch blocks within our program, one of them handles errors that occur when opening a file, and another block handles errors that occur when working with an already opened file.
    14. So let’s create try..catch blocks first, and then we’ll place the rest of the code inside them:
    15. // within the curly braces of the PrintFileInRows method
      try
                  {
                     /* here will be a code, that can have errors that occur 
                     when opening the file*/
                      {
                          try
                          {
                          /* here will be a code, that can have errors that occur 
                         while working with the file*/
                          }
                          catch (IOException e)
                          {
                           // error message when working with the file
                          }
                      }
                  }
                  catch (IOException e)
                  {
                  // error message when opening the file
                  }
              }
         
      
    16. To read something from the file, you first need to open the file. To open the file, it is required to specify the path to the file in your system and the mode — how the operating system should open a file. Place the following code after the open curly brace of the first try..catch block:
    17. using (var f = File.Open(path, FileMode.Open))
      {
      ...
      }
      
    18. After that, we’re going to work with the file, it means that we’ll read some data from a file and to do something with it. So we need to create a variable (br) as an instance of the BinaryReader class, to have the possibility to read data from the file as binary values. The following code has to be placed within the second try..catch block, which is inside of using statement:
    19. // after open curly brace of using statement
       try
           {
               var br = new BinaryReader(f);
      
    20. According to requirements of the task we must output «empty file» message if there is no any data within the file. We’re going to use PeekChar method that returns -1 if the file is empty:
    21. if (br.PeekChar() == -1)
             {
                 WriteLine("empty file");
              }
      
    22. After that, we’re going to read numbers one by one from the file and output them to the console. But we need to output N numbers within each line. So we’ll use the counter i to count how many numbers we’ve already read. If the number of read numbers is equal to N, so we’ll go to the next line:
    23. // after previous if statement
      int i = 0;
      while (br.PeekChar() != -1)
            {
                if (i >= n)
                      {
                        WriteLine();
                        i = 0;
                       }
                 Write(br.ReadInt32() + " ");
                 i++;
             }
      
    24. Within the first catch block, we’re going to print out a message in a case when some error occurs, while working with the file:
    25. // within the first catch block:
      catch (IOException e)
                      {
                          WriteLine($"Error working with file: {e.Message}");
                      }
      
      Class IOException stores the exception that is thrown when an I/O error occurs.
      Message is an attribute of the Exception class that returns the error message that explains the reason for the exception, or an empty string («»).
    26. Within the second catch block, we’re going to print out a message in a case when some error occurs, while opening the file:
    27. // within the first catch block:
      catch (IOException e)
                      {
                          Console.WriteLine($"Error to read from the file: {e.Message}");
                      }
      
    28. At the end of the program code, place the ReadKey method in order not to close the console window while running the program in a debug mode:
    29. // after the closing brace of the catch block:
      ReadKey();
      
      ReadKey method obtains the next character or function key pressed by the user. The pressed key is displayed in the console window.
    30. Press F5 to run the program in a debugging mode and check the output.
    31. To see the result when the error occurs, change the file name, e.g.:
    32. string path = @"L02.dat";
      
    33. Press F5 to run the program in a debugging mode and check the output.
    34. Return the correct name to the file.
    35. To see the result when the error occurs while working with the file, add the next line after the while statement:
    36. // after closing curly brace of the while statement:
      WriteLine(br.ReadInt32());
      
      In this line of code, we try to read from the file when we’ve already read everything.
    37. Press F5 to run the program in a debugging mode and check the output.
    38. Comment the incorrect line of the code.
    39. Download file .cs to the moodle system.
    Task 2:

    To do: Download the file Task02.dat. Paste it into the directory of this task (~/Lesson_14Task2/bin/Debug). There are integer numbers written in the file. Create a program to print these numbers to the console window first, and after that, count the number of numbers, that are smaller than their left neighbor (smaller than the previous number in the sequence of numbers).

    Note 1: Create a method to print out the data from the file:

    static void PrintData(string path)

    Note 2: Create a method to print out the number of numbers smaller than their left neighbor:

    static int CountLessThanLeft(string path)

    Note 3: You may need to use a character encoding while reading from the file:

         using (var fs = new FileStream(path, FileMode.Open))
         using (var br = new BinaryReader(fs, Encoding.ASCII))

    Note 4: There can be multiple try..catch blocks and each of them can handle different errors. Write your code so that one try..catch block handles errors that occur when opening a file, and another block handles errors that occur when working with an already opened file.

    Then the program structure should be as follows:

    ...
    try 
    {
        using ( ... )
        using ( ... )
        { 
            try 
            {
                // work with opened file...
            }
            catch 
            {     
                // handling an error of working with already opened file
            }
       }
    catch 
    {
        // handling an error of opening file
    }

    Note 5: Check to see if the try..catch blocks work properly. Modulate an error while working with already opened file, and error with a name of the file. After checking, return the previous (right) code, and make the comments of the wrong code.

    Expected output:

    The numbers within the file:
    -35 25 28 -37 13 -9 -24 12 -35 37 -24 20 29 -11 -45 -8 43 12 12 44
    The number of numbers smaller than their left neighbor: 8
    +++++++++++++++
    The numbers within the file:
    -35 25 28 -37 13 -9 -24 12 -35 37 -24 20 29 -11 -45 -8 43 12 12 44 Error working with file: Чтение после конца потока невозможно.
    The number of numbers smaller than their left neighbor: 8
    +++++++++++++++
    

    [Solution and Project name: Lesson_14Task2, file name L14Task2.cs]

    FileStream Class: working with bytes

    FileStream class to work with bytes

    Task 3:

    To do: Create a file with a name myFile.txt on your root folder d or c (@"D:\myFile.txt"). Create a program to open the file using FileStream class and put there the numbers: 1 2 3 4 5 6 7 8 9 10 (you should use loop statement and WriteByte() method). Then, read the data from the file and output even numbers to the console window (ReadByte() method should be used).

        
    Expected output:

    result: 
    2 4 6 8 10
    

    [Solution and Project name: Lesson_14Task3, file name L14Task3.cs]

    FileStream class to work with text

    Task 4:

    To do: Create a file with a name myFile.txt on your root folder d or c (@"D:\myFile.txt"). Create a program to open the file using FileStream class and put there some entered string (don’t forget to convert it into a byte array -> Encoding.Default.GetBytes()). Then, read the data from the file and output it to the console window (by converting data again into string -> Encoding.Default.GetString(...)).

        
    Expected output:

    Enter some string:
    Hello, world!
    String from the file:
    Hello, world!
    

    [Solution and Project name: Lesson_14Task4, file name L14Task4.cs]

    * Some theory tips are taken from Mikhalkovish S.S. lections (Rostov Southern Federal University).