前言
以前的文章: Stream 基础和常用 和 IO 常用.
这篇主要是做一个整理, 方便要用的时候 warm up.
之前有讲过 Bit, Byte 的基本概念: Bit, Byte, ASCII, Unicode, UTF, Base64
这里大概复述一下,
计算机最小的单元是 bit, 1 bit 表示一个二进制 (binary), 0 或 1.
byte 是第 2 小的单元, 1 byte = 8 bits. 在 .NET 中经常需要和 bytes 打交道.
二进制是机器看的, 人看的是字符, 所以中间就有了二进制和字符的转换. 那就是 ASCII, Unicode, UTF-8 等等.
.NET 中的 byte, char, string
1 byte = 8 bits, 虽然 8 bits 是二进制, 但是在 Visual Studio debug 的时候显示的是十进制, 0-255 数.
bytes[] 就是一堆 byte 在里面咯.
char 是字符, 1 char = 2 bytes, 可以承载大部分 Unicode 字符, 但不是全部哦. 参考: string and 4-byte Unicode characters 和 How are 4 bytes characters represented in C#
string 是 char 的上层封装. 写业务代码基本上不会碰 char, 一律用 string.
最常见的操作就是在 bytes[] 二进制 和 string 字符串之间做转换. 用到的编码可以是 ASCII, UTF-8 等等
.NET 中的 short, int, long
参考:
MS Dosc – Integral numeric types
short 是 Int16, 16 表示 16 bits, 也就是 2 bytes. 最多能表达 2^16 = 65536 种状态,
由于需要 cover negative number 复数, 所以可用值得范围是 -32768 – 32767 (2^15 = 32768, 减掉 1 位 for zero 所以最大值是 32767)
ushort 是 UInt16 也是 16bits, 但是它不支持 negative number, 所以可用值是 0 – 65535 (2^16 = 65536 减掉 1 位 for zero)
int 是 Int32 概念是一样的, 可用值是 2^31= -2147483648 – 2147483647
long 是 Int64 可用值是 -9223372036854775808 – 9223372036854775807 (天文数字)
uint, ulong 和 ushort 同个原理, 不支持 negetive value.
Bytes and String Conversion
string to bytes
var value = "Hello World"; var bytes = Encoding.UTF-8.GetBytes(value);
Hello World 字母被转换成了 11 个 bytes, 第一个字大写 H 的 byte 是 72. 这个是十进制的表示, 它的二进制是 01001000.
bytes to string
转回去就用 .GetString(bytes)
由于 UTF-8 兼容 ASCII, 所以在 bytes to string 的时候 UTF-8 可以解的出来 ASCII 的 bytes
var value = "Hello World"; var bytes = Encoding.ASCII.GetBytes(value); var value1 = Encoding.UTF8.GetString(bytes); // Hello World
但如果是 UTF-32 就不行了哦
重点
一定要记得, 当要处理 bytes 和字符串的时候, 用什么 encode 一定要搞清楚. 不然很容易就搞成乱码一堆了.
Directory 基本操作
有 2 个 class 负责在 .NET 中管理 directory (folder). 一个是 DirectoryInfo, 另一个是 Directory
Directory 是一个静态类, 里面有一些常用的方法, 比如创建, 删除, copy folder 等等
DirectoryInfo 是一个对 folder 的 reference, 如果只是一次性操作, 通常用静态 Directory 就可以了, 但如果要多次对一个 folder 进行操作那么可以用 DirectoryInfo 对象.
Create Folder
var rootPath = Path.Combine(AppContext.BaseDirectory, @"..\..\..\"); // AppContext.BaseDirectory = C:\...\ProjectFolder\bin\Debug\net6.0\ var directoryInfo = Directory.CreateDirectory(Path.Combine(rootPath, "Parent"));
如果 folder 已经存在, 它就什么也不做, 也不会报错, 也不会覆盖, 就返回 folder 的 reference 而已.
Is Folder Exist
查看 folder 是否已经存在
var folderExist = Directory.Exists(Path.Combine(rootPath, "Parent")); // true
Delete Folder
Directory.Delete(Path.Combine(rootPath, "Parent"), recursive: false);
recursive 是指如果这个 folder 里面有其它 folders 或 files 是否也一起删除, false 表示只要有任何东西就不删除 (如果有它会报错)
如果是 true 就表示删除到干干净净.
默认是 false, 所以 empty folder 才可以直接删除, 不然就要声明 true, 挺安全的.
如果 folder not found 它也会报错哦.
Cut & Paste / Move Folder
Directory.Move(Path.Combine(rootPath, "MyFolder"), Path.Combine(rootPath, @"SomeParent\MyFolder"));
要确保 MyFolder, SomeParent 是存在的, 还有 SomeParent 中不可以有 MyFolder, 不然也会报错.
用 DirectoryInfo 的写法也是差不多的, 这里举一个例子就好.
var dir = new DirectoryInfo(Path.Combine(rootPath, "MyFolder")); dir.MoveTo(Path.Combine(rootPath, @"SomeParent\MyFolder"));
Rename Folder
Directory.Move(Path.Combine(rootPath, "MyFolder"), Path.Combine(rootPath, @"SomeParent\MyRenameFolder"));
rename 其实是通过 move 来实现的, 所以也可以 cut & paste + rename 一起搞.
Get files and subfolders
get files
var fileFullNames = Directory.GetFiles( Path.Combine(rootPath, @"Parent"), searchPattern: "*.txt", searchOption: SearchOption.AllDirectories );
第一个参数是 folder 路径.
searchPattern 是查找的方式, 它不是正则表达哦, 只是可以用 * 和 ? 通配符而已, * 代表正则的 .* 匹配 whatever 任意多个, 或没有, ? 代表正则的 .? 1 个 whatever 或者没有. 没有声明 searchPattern 就是全部 files 都要.
searchOption 可以指定是否查找 descendant folders, 默认是 TopDirectoryOnly 只查询第一层的 files.
注意, 它的返回值是 array of strings, 而不是 FileInfo 对象哦. 它是 FileInfo.FullName, 从 C dirve 的完整 path 路径包括 file name.
get folders
var subfolderFullNames = Directory.GetDirectories( Path.Combine(rootPath, @"Parent") );
get files and folders
var subfolderFullNames = Directory.GetFileSystemEntries( Path.Combine(rootPath, @"Parent") );
parameter 控制方式和 GetFiles 是一样的.
Copy & Paste Folder
并没有 Directory.Copy 这样的接口, File.Copy 就有.... 所以要实现 copy & paste folder 是很不友好的. 参考: stackoverflow – Copy the entire contents of a directory in C# 和 MS Docs – How to: Copy directories
做法就是递归 for loop GetFileSystemEntries > Directory.Create > File.Copy.
Stream
Stream 有点像水, stream 里面是装 bytes 的, bytes 就像鱼儿.
当 stream 静止的时候就像池塘, 里面有很多鱼儿. 当 stream 被传输的时候像水流, 鱼儿会从一个水池被导入进另一个水池中.
.NET Stream 结构
ASP.NET Core build-in 了许多 Stream Class 来处理 Bytes
Stream(抽象类) > TextReader(抽象类) > StreamReader(实体类) > MemoryStream(实体类), FileStream(实体类) 等等
顾名思义, MemoryStream 是负责缓存的, FileStream 是文件的.
File 基本操作
有 3 个 class 经常会用来操作 File.
File, FileInfo, FileStream
File 和 FileInfo 就像 Directory 和 DirectoryInfo 的关系. 一次性操作或者多次操作选择而已.
File 和 Directory 不同, File 是有内容的, 它里面就是一堆的 bytes. 要从 File 里读取 bytes 需要通过 FileStream.
FileStream 提供了很多对 bytes 的控制, 比如读多少, 从哪里开始读, 写入的时候先写入内存, 确定后才写入磁盘等等.
Create File
using var fileStream = File.Create(Path.Combine(rootPath, "Text.txt"));
它返回的是 fileStream 而不是 FileInfo 哦, 通常创建后就是要写入嘛.
FileStream 是 IDisposable 所以必须配上 using, 使用完后要释放.
Is File Exist
var isFileExist = File.Exists(Path.Combine(rootPath, "Text.txt"));
Delete File
File.Delete(Path.Combine(rootPath, "Text.txt"));
如果 file 不存在, 它不会报错哦, 这个 Directory 是不同的.
Cut & Paste / Rename / Move File
File.Move(Path.Combine(rootPath, "Text.txt"), Path.Combine(rootPath, "Text123.txt"));
和 Directory 一样通过 move 来实现 rename.
Copy & Paste File
File.Copy(Path.Combine(rootPath, "Text.txt"), Path.Combine(rootPath, "Text123.txt"));
Directory 没有 build-in 的 copy & paste, 但 File 有. 如果 paste 的文件名字已经存在, 会报错哦.
FileInfo 常用属性
var fileInfo = new FileInfo(Path.Combine(rootPath, "Text.txt")); var isFileExist = fileInfo.Exists; // true var fullName = fileInfo.FullName; // C:\...\Folder\Text.txt var name = fileInfo.Name; // Text.txt var extension = fileInfo.Extension; // .txt var fileLength = fileInfo.Length; // 12 (单位是 bytes) var directoryFullName = fileInfo.DirectoryName; // C:\...Folder var directoryInfo = fileInfo.Directory; var creationTime = fileInfo.CreationTime; // DateTime var creationTimeUtc = fileInfo.CreationTimeUtc; // DateTime var lastAccessTime = fileInfo.LastAccessTime; // DateTime var lastWriteTime = fileInfo.LastWriteTime; // DateTime
File Open
using var fileStream = File.Open(Path.Combine(rootPath, "Text.txt"), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
第一个参数是文件路径.
第二个 FileMode 是指打开的方式, 它有几个选择, 比如 CreateNew, Create, Open, OpenOrCreate 等等. 它们之间差不多但又有点不同, 要用的时候去看一下就可以了.
比如 CreateNew 明确说明要创建, 如果已经有了会报错. Create 则不会报错.
第三个 FileAccess 是指打开的目的, Read, Write, ReadWrite.
第四个 FileShare 是指, 当打开的时候其它访问可以接受吗? 比如当写入的时候, 或许不希望其它访问. 这样容易造成并发混乱.
FileStream
Read 场景:
有一个 Text.txt 的文件, 里面的内容是 abc我你他, 需要把第 2 个字 "b" 读出来.
OpenRead FileStream
var rootPath = Path.Combine(AppContext.BaseDirectory, @"..\..\..\"); // AppContext.BaseDirectory = C:\...\ProjectFolder\bin\Debug\net6.0\ using var fileStream = File.OpenRead(Path.Combine(rootPath, "Text.txt")); // 内容 = "abc我你他"
通过 OpenRead 开启对 File 的读. 返回的 fileStream 是用来控制 bytes 的.
prepare empty buffer (array of bytes)
先准备一个容器 array of bytes.
var secondWordByte = new byte[1];
adjust FileStream position (针头)
调整 FileStream 的针头, 要读第 2 个字出来, 所以是从最开头偏移 1 个 byte. SeekOrigin.Current 表示从当前的针头位置 (目前没有移动过所以它就是起点)
var p1 = fileStream.Position; // 0 fileStream.Seek(offset: 1, SeekOrigin.Current); var p2 = fileStream.Position; // 1
常见的调整 position 方法有:
fileStream.Seek(offset: 1, SeekOrigin.Current); // 从当下开始偏移 fileStream.Seek(offset: 1, SeekOrigin.Begin); // 从前面开始偏移 fileStream.Seek(offset: 1, SeekOrigin.End); // 从后面开始偏移
read bytes from file stream to buffer
接着读取 bytes 放入准备好的容器
fileStream.Read(secondWordByte, offset: 0, count: 1);
读入容器的时候也可以调整偏移 (目前没有需要), count: 1 表示读 1 个 byte 装入容器.
read 完以后, fileStream 的针头位置就去到第 2 位了, buffer 就有 bytes 了 (注: 它是 copy 过去而不是 cut 哦, 所以 stream 里面依然有 bytes)
fileStream.Seek(offset: 1, SeekOrigin.Current); fileStream.Read(secondWordByte, offset: 0, count: 1); var p3 = fileStream.Position; // 2
所有 stream 的操作都是这样的, 读写都是. 一定要有 position 的概念. 读写完成后 position 会自动跑位, 比如当前在 5, 读 3 bytes, 那么它就去到 8, 再写入 2 个 bytes 它就变成 10.
read step: prepare empty buffer > 调整 stream positon > read from stream to 容器 (指定 read how many bytes and offset).
write step: prepare data buffer > 调整 stream position (通常是 set to end) > write from buffer to stream (指定 write how many bytes and offset).
convert byte to string
接着, 把 bytes 转换成字符串, 就可以了. 必须清楚这个 file 里面的 bytes 是用什么编码的, 不然就解不出来了.
var value = Encoding.UTF8.GetString(secondWordByte); // "b"
read all bytes or text
如果只是想简单的读到完, 可以直接调用 ReadAllBytes
var bytes = File.ReadAllBytes(Path.Combine(rootPath, "Text.txt")); var value = Encoding.UTF8.GetString(bytes); // abc我爱你
甚至可以直接 read all text
var value = File.ReadAllText(Path.Combine(rootPath, "Text.txt"), Encoding.UTF8);
底层原理是一样的, 只是一个封装而已.
Write 的场景:
写和读区别不到, 直接看代码就可以了
var rootPath = Path.Combine(AppContext.BaseDirectory, @"..\..\..\"); using var fileStream = File.OpenWrite(Path.Combine(rootPath, "Text.txt")); var buffer = Encoding.UTF8.GetBytes("abc我爱你"); fileStream.Seek(0, SeekOrigin.End); fileStream.Write(buffer); fileStream.Flush();
唯一特别的是 Flush. 当调用 Write 的时候, buffer 并没有马上被写入到磁盘. 而是在内存中.
调用 Flush 可以立刻让它写入磁盘里. 一般上是不需要去调用它的, 因为在 fileStream displose 的时候它会自动去做写入磁盘这个动作.
clear stream
fileStream.SetLength(0)
巩固记忆:
总之, 记住几个东西, 就不会搞混了.
1 个 stream
1 个 buffer
buffer 就是 bytes[]
stream 里面也是 bytes[]
一般上:
read 的情况, buffer 是空的, 从 stream 读出来装入 buffer. 过程中可以调整双方的 offset. 可以控制读多少 bytes 过去.
write 的情况复杂一点, buffer 是有 bytes 的 (准备写入的 bytes). stream 有可能是空的, 也有可能有 data. 通常会往后继续增加 (append 的概念). 所以会先把 stream 调到 SeekOrigin.End 然后写入. 过程中也是可以调整 offset. 可以控制写多少 bytes 过去.
MemoryStream
它的读写方式和 FileStream 基本上是一样的. 只是没有 Flush 的概念. 因为它只是内存而已, 跟磁盘没有关系.