c# 高级编程 22章504页 【文件和流】【流】

345 阅读6分钟

处理文件 的 更强大的选项:流

流:是一个用于 传输数据 的对象。数据可以 向两个方向 传输

  • 外部源 传输到 程序中:读取流
  • 程序 传输到 外部源中:写入流

使用 一个独立的对象传输数据使用FileInfoDirectoryInfo更好

  • 把 传输数据的概念 与 特定数据源 分离开来,可以更容易地 交换数据源

外部源 可以是以下情况:

  • 文件
  • 使用一些 网络协议 读写 网络上的数据。目的是:选择数据或从另一个计算机上发送数据
  • 把数据 读写到 命名管道
  • 把数据 读写到 一个内存区域
  • ...

一些流 只允许写入,一些流 只允许读取

一些流 允许 随机存取。 随即存取:在流中 随机 定位游标

  • 例如:从流的开头开始读取,之后移动到流的末尾,再从流的一个中间位置继续读取

  • System.IO.MemoryStream对象:读写内存
  • System.Net.Sockets.NetworkStream对象:处理网络数据

System.IO.Stream 类:

  • 外部源 不做任何限定,外部源 可以是:
    • 文件流
    • 内存流
    • 网络流
    • 任意数据源
      • 例如:外部源是代码中的一个变量。使用流 在变量之间 传输数据 很有用,可以 在数据类型之间 转换数据
  • 包含 很多通用代码,可以在外部源和代码中的变量之间移动数据,把这些代码与特定数据源的概念 分离开来,更容易实现 不同环境下的 代码重用
  • 直接读写流,不是那么容易可以使用 阅读器 和 写入器。这也是一个关注点的分离。
    • StringReaderStringWriter
    • StreamReaderStreamWriter

一些流 可以 链接 起来。例如,可以链接DeflateStreamCryptoStream,再写入FileStream

  • DeflateStream压缩数据
    • 这个流可以写入FileStreamMemoryStreamNetworkStream
  • CryptoStream: 加密数据

文件读写 的常用类

  • FileStream: 在 二进制文件中 读写 二进制 数据
  • StreamReaderStreamWriter:读写 文本格式的 流产品
  • BinaryReaderBinaryWriter:读写 二进制格式的 流产品

使用这些类 和 直接使用 底层的流对象 之间的区别:基本流 是按照字节来工作的 例如:

  • BinaryReader.Write()一个重载的的参数是long类型,long占8个字节,它把8个字节写入流中
  • BinaryReader.Read()就是,从流中提取8个字节,恢复long的值

FileStream

创建FileStream

有很多构造函数。其中第一个,有以下4个参数:

  • 参数1:字符串 表示 文件的完整路径
  • 参数2:打开文件的模式
    • 新建一个文件
    • 打开一个现有文件
      • 写入操作 覆盖原来的内容
      • 写入操作 追加到文件末尾
    • 枚举 FileMode:
      • Append:文件不存在时,抛异常
      • Create:不管文件是否存在,都不抛异常。删除任何现有文件,新建一个空文件
      • CreateNew:文件存在时,抛异常
      • Open:文件不存在时,抛异常
      • OpenOrCreate:不管文件是否存在,都不抛异常。
      • Truncate:文件不存在时,抛异常
    • FileMode与文件现有状态不一致:会抛异常,如上
  • 参数3:访问文件的方式
    • 只读,只写,读写
    • 枚举FileAccess:
      • Read
      • ReadWrite
      • Write
    • FileAccess是按位标志,可以和按位运算符 | 一起使用
  • 参数4:是否独占文件
    • 不允许 其他流 同时访问文件
    • 允许 其他流 同时访问文件
      • 其他流 只读
      • 其他流 只写
      • 其他流 读写
    • 枚举FileShare:
      • Delete
      • Inheritable
      • None
      • Read
      • ReadWrite
      • Write
    • FileShare是按位标志,可以和按位运算符 | 一起使用
var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read)

File.OpenFile()

File.OpenFile()也能返回FileStream

FileStream stream = File.OpenFile(file);

获取流的信息

Stream类定义了以下属性:

  • CanRead
  • CanWrite
  • CanSeek
  • CanTimeout
    • 读写流,可以用ReadTimeoutWriteTimeout来指定超时(毫秒)
  • Position
    • 返回 当前 光标 在流中的位置
    • 每次 从流中读取 一些数据,光标的位置 就移动到 下一个 将读取的字节上
        public static void ShowStreamInformation(Stream stream)
        {
            Console.WriteLine($"stream can read: {stream.CanRead}, can write: {stream.CanWrite}, can seek: {stream.CanSeek}, can timeout: {stream.CanTimeout}");
            Console.WriteLine($"length: {stream.Length}, position: {stream.Position}");
            if (stream.CanTimeout)
            {
                Console.WriteLine($"read timeout: {stream.ReadTimeout} write timeout: {stream.WriteTimeout} ");
            }
        }

输出:

stream can read: True, can write: False, can seek: True, can timeout: False
length: 431, position: 0

Read()ReadByte()

读取一个流的时候:

  • ReadByte():从流中只读取一个字节
  • Read(): 从流中读取一个字节 数组
    • 返回读取的字节数
    • 如果没有更多可用于读取,就返回0
    • 流可能小于缓冲区

利用StreamRead() 分析文本文件的字符编码方式

序言:读取流中的前几个字节

  • 提供了文本文件的字符如何编码的信息
  • 也叫字节顺序标记 (Byte Order Mark, BOM)

文本文件的 字符编码 方式:

  • ASCII
    • 每一个字符使用7位来进行表示
    • 基于字母表,提供了
      • 小写字母
      • 大写字母
      • 控制字符
  • UTF-32
    • 前两个字节是 FF FE
    • 第三个字节和第四个字节 都是0
    • 前四个字节加起来一共32位
    • 每一个字符使用32位来进行表示
  • UTF-16 小端序
    • 是两字节的Unicode
    • 前两个字节是 FF FE (FF在FE之前,表示小端序)
    • 第三个字节和第四个字节 都不为0
    • 前两个字节加起来一共16位
    • 每一个字符使用16位来进行表示
  • UTF-16 大端序
    • 是两字节的Unicode
    • 前两个字节是 FE FF (FE在FF之前,表示大端序)
    • 第三个字节和第四个字节 都不为0
    • 前两个字节加起来一共16位
    • 每一个字符使用16位来进行表示
  • UTF-8:
    • 目前最常用的 文本文件字符编码格式
    • 第一个字节是EF
    • 第二个字节是BB
    • 第三个字节是BF
    • 每个字符使用 1到6个字节 即8到48位来进行表示
        public static Encoding GetEncoding(Stream stream)
        {
            if (!stream.CanSeek) throw new ArgumentException("require a stream that can seek");

            Encoding encoding = Encoding.ASCII;

            byte[] bom = new byte[5];
            int nRead = stream.Read(bom, offset: 0, count: 5);
            if (bom[0] == 0xff && bom[1] == 0xfe && bom[2] == 0 && bom[3] == 0)
            {
                Console.WriteLine("UTF-32");
                stream.Seek(4, SeekOrigin.Begin);
                return Encoding.UTF32;
            }
            else if (bom[0] == 0xff && bom[1] == 0xfe)
            {
                Console.WriteLine("UTF-16, little endian");
                stream.Seek(2, SeekOrigin.Begin);
                return Encoding.Unicode;
            }
            else if (bom[0] == 0xfe && bom[1] == 0xff)
            {
                Console.WriteLine("UTF-16, big endian");
                stream.Seek(2, SeekOrigin.Begin);
                return Encoding.BigEndianUnicode;
            }
            else if (bom[0] == 0xef && bom[1] == 0xbb && bom[2] == 0xbf)
            {
                Console.WriteLine("UTF-8");
                stream.Seek(3, SeekOrigin.Begin);
                return Encoding.UTF8;
            }
            stream.Seek(0, SeekOrigin.Begin);
            return encoding;
        }

读取流

File.OpenWrite() 创建一个 可以写入 的流 FileStream

示例

  • Path.GetTempFileName() 创建 临时文件名
  • Path.ChangeExtension() 修改 文件扩展名
  • 写入UTF-8文件时,需要写入 序言
    • 要么,直接写入 字节
      • 调用WriteByte()给流 发送 三个字节的UTF-8 序言
    • 要么,使用Encoding
      • 无序记住UTF-8所对应的序言字节,Encoding类有这些信息
      • GetPreamble()返回一个字节数组,包含序言
      • 调用Write()写入此字节数组
  • 写入 文件内容
    • Encoding.UTF8.GetBytes()得到 字节数组
    • Write()写入 字节数组
  • 用编辑器打开,它会使用 正确的编码 UTF-8
        public static void WriteTextFile()
        {
            string tempTextFileName = Path.ChangeExtension(Path.GetTempFileName(), "txt");
            using (FileStream stream = File.OpenWrite(tempTextFileName))
            {
                //// write BOM
                //stream.WriteByte(0xef);
                //stream.WriteByte(0xbb);
                //stream.WriteByte(0xbf);

                byte[] preamble = Encoding.UTF8.GetPreamble();
                stream.Write(preamble, 0, preamble.Length);

                string hello = "Hello, World!";
                byte[] buffer = Encoding.UTF8.GetBytes(hello);
                stream.Write(buffer, 0, buffer.Length);
                Console.WriteLine($"file {stream.Name} written");
            }
        }

复制流

  • Stream.Read(): 读取缓冲区
  • Stream.Write():写入缓冲区
        public static void CopyUsingStreams(string inputFile, string outputFile)
        {
            const int BUFFERSIZE = 4096;
            using (var inputStream = File.OpenRead(inputFile))
            using (var outputStream = File.OpenWrite(outputFile))
            {
                byte[] buffer = new byte[BUFFERSIZE];
                bool completed = false;
                do
                {
                    int nRead = inputStream.Read(buffer, 0, BUFFERSIZE);
                    if (nRead == 0) completed = true;
                    outputStream.Write(buffer, 0, nRead);
                } while (!completed);
            }
        }
  • 为了复制流,不需要编写上面的代码,可以直接使用Stream类的CopyTo()方法
        public static void CopyUsingStreams2(string inputFile, string outputFile)
        {
            using (var inputStream = File.OpenRead(inputFile))
            using (var outputStream = File.OpenWrite(outputFile))
            {
                inputStream.CopyTo(outputStream);
            }
        }

随机访问 流

随即访问流(甚至可以访问大文件)的一个优势:可以快速访问文件中的特定位置

示例

  • 创建一个大文件
        public static async Task CreateSampleFileAsync(int nRecords)
        {
            using (FileStream stream = File.Create(SampleFileDataPath))
            using (var writer = new StreamWriter(stream))
            {
                var r = new Random();

                var records = Enumerable.Range(1, nRecords).Select(x => new
                {
                    Number = x,
                    Text = $"Sample text {r.Next(200)}",
                    Date = new DateTime(Math.Abs((long)((r.NextDouble() * 2 - 1) * DateTime.MaxValue.Ticks)))
                });

                foreach (var rec in records)
                {
                    string date = rec.Date.ToString("d", CultureInfo.InvariantCulture);
                    string s = $"#{rec.Number,8};{rec.Text,-20};{date}#{Environment.NewLine}";
                    await writer.WriteAsync(s);
                }
            }
        }
  • 用以上代码可以尝试创建一个包含150万条记录或更多的大文件
  • 用记事本打开这个大文件会比较慢,但用随机存取会非常快

关于上面这一段code, 如果写成下面这样:

        public static async Task CreateSampleFileAsync(int nRecords)
        {
            FileStream stream = File.Create(SampleFileDataPath)
            using (var writer = new StreamWriter(stream))
            {
                
            }
        }
  • 每个实现IDisposable的对象 都应该销毁。如FileStream
  • StreamWriter销毁时,StreamWriter会控制所使用的资源,并销毁流
  • 为了使流打开得时间比StreamWriter更长,可以使用StreamWriter的构造函数配置它
  • 这种情况下,需要显式地销毁流

把流标定位到流中的一个随机位置,读取记录。

  • 用户输入想访问的Record号
  • 流中 要访问的字节 是基于Record号和Record大小 StreamSeek()方法
  • 第一个参数:定位流中的光标
  • 第二个参数:表示 流的开头、流的末尾、当前位置
        public static void RandomAccessSample()
        {
            try
            {
                using (FileStream stream = File.OpenRead(SampleFileDataPath))
                {
                    byte[] buffer = new byte[RECORDSIZE];
                    do
                    {
                        try
                        {
                            Console.Write("record number (or 'bye' to end): ");
                            string line = Console.ReadLine();
                            if (line.ToUpper().CompareTo("BYE") == 0) break;

                            if (int.TryParse(line, out int record))
                            {
                                stream.Seek((record - 1) * RECORDSIZE, SeekOrigin.Begin);
                                stream.Read(buffer, 0, RECORDSIZE);
                                string s = Encoding.UTF8.GetString(buffer);
                                Console.WriteLine($"record: {s}");
                            }
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine(ex.Message);
                        }
                    } while (true);
                    Console.WriteLine("finished");
                }
            }
            catch (FileNotFoundException)
            {
                Console.WriteLine("Create the sample file using the option -sample first");
            }
        }

使用缓存 的流

从性能原因上看,在读写文件时,输出结果会被缓存 例如:当程序要读取文件流中的下两个字节:

  • 文件流 会把请求 传递给Windows
    • 如果不使用缓存,则会
      • Windows连接文件系统
      • 定位文件
      • 从磁盘中读取文件,仅读取两个字节
    • 如果使用缓存,则会:
      • 在一次读取过程中,检索文件中的一个大块
      • 将该大块保存在一个内存区域,即缓冲区上
      • 以后对流中数据的请求 就会从该缓冲区中读取,直到读完该缓冲区位置
      • 这时,Windows会从文件中再获取另一个大块 例如:当程序要写入字节,与上面的方式相同。

对于文件,操作系统会自动完成缓冲区的填充和清空操作。

对于 从 没有缓存的地方 读取数据

  • 首先需要创建一个流
  • 再从BufferedStream创建一个流,BufferedStream实现一个缓冲区
    • BufferStream 并不用于 程序 频繁切换 读数据和写数据 的情形
  • 将第一步创建的需要缓存的流传递给BufferStream的构造函数

使用 读取器 和 写入器

FileStream类 读写文本文件 需要使用 字节数组 和 处理编码

其实有 更简单的方法: 使用 读取器StreamReader 和 写入器StreamWriter 来读写FileStream 。不需要处理 字节数组 和 编码


StreamWriterStreamReader的 工作级别 比较高

  • 其实现方式 可以根据 流的内容,自动检测 停止读取文本比较方便的位置
    • 一次读写一行文本
      • 会自动确定 下一个回车符 的位置,并在该处 停止读取
    • 写入文件时
      • 会自动把回车符 和 换行符 追加到 文本的末尾
  • 不需要担心 文件中所使用的编码方式
    • StreamReader 默认使用 UTF-8
    • StreamReader 有重载的构造函数 可以指定使用文件中序言定义的编码
    • StreamReader 有重载的构造函数 可以显式地指定一个编码
// 使用文件序言中定义的编码
var reader = new StreamReader(stream, detectEncodingFromByteOrderMarks: true);
// 显式地指定一个编码
var reader = new StreamReader(stream, Encoding.Unicode);

StreamReader

  • 构造接收FileStream
  • EndOfStream 检查是否到达文件的末尾
  • 读取方式:
    • ReadLine() 读取文本行
    • ReadToEnd() 从光标处读取到末尾
    • Read() 把内容读入一个字符char数组(不是字节数组)。char类型使用两个字节。
      • 适合16位的Unicode
      • 不适合UTF-8 (一个字符可以是1到6个字节)
  • 有重载的构造函数 可以指定编码方式
    • 默认是UTF-8
  • 有重载的构造函数 可以指定 要使用的缓冲区
    • 默认为 1024字节
  • 还可以指定 关闭StreamReader时,不应该关闭底层流FileStream
    • 默认是 关闭StreamReader(即调用Dispose方法),会关闭底层流
  • 初始化方式:
    • new StreamReader()
      • 接收一个FileStream
    • File.OpenText()创建一个StreamReader()
        public static void ReadFileUsingReader(string fileName)
        {
            var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read);
            using (var reader = new StreamReader(stream))
            {
                while (!reader.EndOfStream)
                {
                    string line = reader.ReadLine();
                    Console.WriteLine(line);
                }
            }
        }

StreamWriter

StreamReader工作方式相同。只不过 仅用于写入文件 (或写入另一个流)

  • 构造函数有17个版本的重载。
    • 默认以UTF-8格式写入文本内容
    • 可以设置Encoding
    • 可以指定缓冲区大小
    • 可以指定关闭写入器时,是否关闭底层流
    • 允许传递字符串和一些.NET数据类型
      • 会使用指定的编码 变成字符串
        public static void WriteFileUsingWriter(string fileName, string[] lines)
        {
            var outputStream = File.OpenWrite(fileName);
            using (var writer = new StreamWriter(outputStream))
            {
                byte[] preamble = Encoding.UTF8.GetPreamble();
                outputStream.Write(preamble, 0, preamble.Length);
                writer.Write(lines);
            }
        }

BinaryWriter

读写二进制文件的另一种选择是 直接使用 流类型。这时,最好使用 字节数组 执行读写操作。

另一种选择是 使用专为这个场景定义的 读取器 BinaryReader和写入器BinaryWriter

BinaryWriter 以二进制格式 写入 数据类型

  • 不使用 任何编码
  • 文件使用二进制格式 而不是 文本格式 写入
  • 18个版本的Write()。接收不同的类型 double, int, long, string
        public static void WriteFileUsingBinaryWriter(string binFile)
        {
            var outputStream = File.Create(binFile);
            using (var writer = new BinaryWriter(outputStream))
            {
                double d = 47.47;
                int i = 42;
                long l = 987654321;
                string s = "sample";

                writer.Write(d);
                writer.Write(i);
                writer.Write(l);
                writer.Write(s);
            }
        }

  • 读取文件的顺序, 必须完全匹配 写入的顺序
  • 创建自己的二进制格式时,需要知道存储的内容和方式,并用相应的方式读取
        public static void ReadFileUsingBinaryReader(string binFile)
        {
            var inputStream = File.Open(binFile, FileMode.Open);
            using (var reader = new BinaryReader(inputStream))
            {
                double d = reader.ReadDouble();

                int i = reader.ReadInt32();
                long l = reader.ReadInt64();
                string s = reader.ReadString();
                Console.WriteLine($"d: {d}, i: {i}, l: {l}, s: {s}");
            }
        }

压缩文件

.NET提供的类:包含不同的算法 压缩和解压 流。

  • DeflateStream
    • 压缩和解压 流
  • GZipStream
    • 压缩和解压 流
    • 在后台使用DeflateStream,算法一样。
      • Brotli算法:谷歌新开源压缩算法。
    • 增加了循环冗余校验,来检测数据的损坏情况
  • ZipArchive
    • 创建和读取Zip文件
      • Windows能直接打开zip文件
      • Windows不能打开gzip文件, 需要第三方工具

示例:

流的一个特性:它们可以链接起来

DeflateStream

  • CompressMode.Compress表示压缩
  • 使用Write()方法 或 其他方法 写入DeflateStream。这里用CopyTo()方法写入这个DeflateStream
        public static void CompressFile(string fileName, string compressedFileName)
        {
            using (FileStream inputStream = File.OpenRead(fileName))
            {
                FileStream outputStream = File.OpenWrite(compressedFileName);
                using (var compressStream = new DeflateStream(outputStream, CompressionMode.Compress))
                {
                    inputStream.CopyTo(compressStream);
                }
            }
        }
  • CompressMode.Decompress表示解压缩
        public static void DecompressFile(string fileName)
        {
            FileStream inputStream = File.OpenRead(fileName);
            using (MemoryStream outputStream = new MemoryStream())
            using (var compressStream = new DeflateStream(inputStream, CompressionMode.Decompress))
            {
                compressStream.CopyTo(outputStream);
                outputStream.Seek(0, SeekOrigin.Begin);
                using (var reader = new StreamReader(outputStream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 4096, leaveOpen: true))
                {
                    string result = reader.ReadToEnd();
                    Console.WriteLine(result);
                }
            }
        }

BrotliStream

  • 使用上,和DeflateStream类似
  • 添加Nuget包:`System.IO.Compression.Brotli
        public static void CompressFileWithBrotli(string fileName, string compressedFileName)
        {
            using (FileStream inputStream = File.OpenRead(fileName))
            {
                FileStream outputStream = File.OpenWrite(compressedFileName);
                using (var compressStream = new BrotliStream(outputStream, CompressionMode.Compress))
                {
                    inputStream.CopyTo(compressStream);
                }
            }
        }
        public static void DecompressFileWithBrotli(string fileName)
        {
            FileStream inputStream = File.OpenRead(fileName);
            using (MemoryStream outputStream = new MemoryStream())
            using (var compressStream = new BrotliStream(inputStream, CompressionMode.Decompress))
            {
                compressStream.CopyTo(outputStream);
                outputStream.Seek(0, SeekOrigin.Begin);
                using (var reader = new StreamReader(outputStream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 4096, leaveOpen: true))
                {
                    string result = reader.ReadToEnd();
                    Console.WriteLine(result);
                }
            }
        }

ZipArchive

  • 包含多个ZipArchiveEntry对象。为每个文件创建一个ZipArchiveEntry对象。
  • 不是一个流
  • 但使用流 进行读写
        public static void CreateZipFile(string directory, string zipFile)
        {
            InitSampleFilesForZip(directory);
            string destDirectory = Path.GetDirectoryName(zipFile);
            if (!Directory.Exists(destDirectory))
            {
                Directory.CreateDirectory(destDirectory);
            }
            FileStream zipStream = File.Create(zipFile);
            using (var archive = new ZipArchive(zipStream, ZipArchiveMode.Create))
            {
                IEnumerable<string> files = Directory.EnumerateFiles(directory, "*", SearchOption.TopDirectoryOnly);
                foreach (var file in files)
                {
                    ZipArchiveEntry entry = archive.CreateEntry(Path.GetFileName(file));
                    using (FileStream inputStream = File.OpenRead(file))
                    using (Stream outputStream = entry.Open())
                    {
                        inputStream.CopyTo(outputStream);
                    }
                }
            }
        }

        private static void InitSampleFilesForZip(string directory)
        {
            if (!Directory.Exists(directory))
            {
                Directory.CreateDirectory(directory);

               for (int i = 0; i < 10; i++)
                {
                    string destFileName = Path.Combine(directory, $"test{i}.txt");

                    File.Copy("Test.txt", destFileName);
                }

            } // else nothing to do, using existing files from the directory
        }

FileSystemWatcher 观察文件的更改

  • 事件触发,在
    1. 创建 文件:FileSystemEventHandler
    2. 删除 文件:FileSystemEventHandler
    3. 更改 文件:FileSystemEventHandler
    4. 重命名 文件:RenamedEventHanlder 派生自 FileSystemEventHandler
  • 使用场景 举例:
    • 服务器上传文件时
    • 文件缓存在内存中,缓存需要在文件更新时失效
  • FileSystemWatcher
    • new FileSystemWatcher(".", "*.txt")
      • 指定该观察的目录
      • 指定过滤器
    • IncludeSubdirectories = true
      • 指定 是否观察子目录中的文件
    • EnableRaisingEvents = true
      • 需要 调用
    • Dispose()
      • 需要 调用
using System;
using System.IO;

namespace FileMonitor
{
    public class Program
    {
        private static FileSystemWatcher s_watcher;

        public static void Main(string[] args)
        {
            WatchFiles(".", "*.txt");
            Console.ReadLine();
            UnWatchFiles();
        }

        public static void WatchFiles(string path, string filter)
        {
            s_watcher = new FileSystemWatcher(path, filter)
            {
                IncludeSubdirectories = true
            };
            s_watcher.Created += OnFileChanged;
            s_watcher.Changed += OnFileChanged;
            s_watcher.Deleted += OnFileChanged;
            s_watcher.Renamed += OnFileRenamed;

            s_watcher.EnableRaisingEvents = true;
            Console.WriteLine("watching file changes...");
        }

        public static void UnWatchFiles()
        {
            s_watcher.Created -= OnFileChanged;
            s_watcher.Changed -= OnFileChanged;
            s_watcher.Deleted -= OnFileChanged;
            s_watcher.Renamed -= OnFileRenamed;
            s_watcher.Dispose();
            s_watcher = null;
        }

        private static void OnFileRenamed(object sender, RenamedEventArgs e) =>
            Console.WriteLine($"file {e.OldName} {e.ChangeType} to {e.Name}");

        private static void OnFileChanged(object sender, FileSystemEventArgs e) =>
            Console.WriteLine($"file {e.Name} {e.ChangeType}");
    }
}