C# 文件流:Stream篇(一)
C# 文件流:Stream篇(一)
前话:
本文系列本着备忘的目的进行归纳,Stream系列原文链接:C# 温故而知新:Stream篇(—) - 逆时针の风 - 博客园 (cnblogs.com) 望各位看官到原作者处学习。
后几篇不作注释,还请见谅
--------------------------------------------------------------------------------------------分割线------------------------------------------------------------------------------------------------------
什么是Stream?
提供字节序列的一般视图
那什么是字节序列呢?
字节对象都被存储为连续的字节序列,字节按照一定的顺序进行排序,组成了字节序列
马上进入正题,C#中 Stream 是如何使用的
Stream 类是一个抽象类,无法直接如下使用创建实例
Stream stream = new Stream();
因此我们自定义一个流继承 Stream ,查看哪些属性必须重写或者自定义
public class StreamEx : Stream
{
public override bool CanRead => throw new NotImplementedException();
public override bool CanSeek => throw new NotImplementedException();
public override bool CanWrite => throw new NotImplementedException();
public override long Length => throw new NotImplementedException();
public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public override void Flush()
{
throw new NotImplementedException();
}
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotImplementedException();
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotImplementedException();
}
public override void SetLength(long value)
{
throw new NotImplementedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotImplementedException();
}
}
可看出,系统会自动帮助我们实现 Stream 抽象属性和方法
1. CanRead:只读属性,判断该流是否支持读取
2. CanSeek:只读属性,判断该流是否支持查找功能
3. CanWrite:只读属性,判断该流是否支持写入
4. Length:表示流长度(以字节为单位)
*5. Position属性:(重要)
从字面意思看,Position属性表示流中的当前位置。但是当 Stream 对象被缓存后,导致 Position 属性在流中无法正确找到对应的位置,其实解决这个问题很简单,我们每次在使用流前,将 Stream.Position 设置为 0 就可以了,但是这不能从根本上解决,最好的方法就是用 Using 语句将流对象包裹起来,用完后关闭回收即可
*6. void Flush():
当我们使用流写文件时,数据流会先进入到缓冲区中,而不会立刻写入文件,当执行这个方法后,缓冲区的数据流会立即注入基础流
MSDN中描述为:使用此方法将所有信息从基础缓冲区移动到其目标或清除缓冲区,或者同时执行这两种操作。根据对象的状态,可能需要修改流内的位置
当使用 StreamWriter 或者StreamReader 类时,不要刷新 Stream 基对象,而应使用该类的 Flush 或者 Close 方法,此方法确保首先将该数据刷新至基础流,然后再将其写入文件
*7. abstract int Read(byte[] buffer, int offset, int count)
这个方法包含3个关键参数:缓冲字节数据,字节偏移量,读取的字节数。每次读取一个字节后会返回一个缓冲区中的总字节数
第一个参数(byte[] buffer):这个数组相当于一个空盒子,read() 方法每次读取流中的一个字节将其放进这个空盒子里,全部读完后便可使用 buffer 字节数组了
第二个参数(int offset):表示位移偏移量,告诉我们从流中的哪个位置(偏移量)开始读取
第三个参数(int count):读取多少个字节数
返回值:总共读取多少字节数
*8. abstract long Seek(long offset, SeekOrigin origin)
不知是否还记得 Position 属性不?其实 Seek 方法就是重新设定流中的一个位置,在说明 offset 参数作用之前,大家可先了解下 SeekOrigin 这个枚举:

如果 offset 为负,则要求新位置位于 origin 指定的位置之前,其间隔相差 offset 指定的字节数
如果 offset 为零(0),则要求新位置位于由 origin 指定的位置处
如果 offset 为正,则要求新位置位于 origin 指定的位置之后,其间隔相差 offset 指定的字节数
Stream. Seek(-3,Origin.End); //表示在流末端往前数第3个位置 Stream. Seek(0,Origin.Begin); //表示在流的开头位置 Stream. Seek(3,Orig`in.Current); //表示在流的当前位置往后数第三个位置
查找之后会返回一个流中的一个新位置
9. void SetLength(long value)
设置当前流的长度
参数(value):所需的当前流的长度(以字节表示)
*10. abstract void Write(byte[] buffer, int offset, int count)
这个方法包含3个关键参数:缓冲字节数据,字节偏移量,读取的字节数。
第一个参数(byte[] buffer):这个数组在使用时就已经有了许多 byte 类型
第二个参数(int offset):表示位移偏移量,告诉我们从流中的哪个位置(偏移量)开始写入
第三个参数(int count):写入多少个字节数
*11. virtual void Close()
关闭流并释放资源,在实际操作中,如果不用 using 语句的话,别忘了使用完流之后将其关闭
这个方法非常重要,使用完当前流别忘记关闭流!
为了更明确Stream的属性和方法,请看示例
static void Main(string[] args)
{
try
{
byte[] readBuffer = null;
char[] readCharBuffer = null;
string messageString = "Stream Practice!";
string newMessageString = string.Empty;
using (MemoryStream memoryStream = new MemoryStream())
{
Console.WriteLine($"初始字符串:{messageString}");
//如果该流允许写入
if (memoryStream.CanWrite)
{
//首先尝试将字符串 messageString 写入流中
//通过 Encoding 实现 string -> byte[] 的转换
byte[] buffer = Encoding.Default.GetBytes(messageString);
//我们从该数组的第一个位置开始写,长度为10,写完之后 memoryStream中便有了数据
//比较难以理解的是,数据是什么时候写入到流中的,在冗长的项目代码里面,都会有这个问题
memoryStream.Write(buffer, 0, 10);
Console.WriteLine($"现在 Stream.Position 在第{memoryStream.Position + 1}位置");
//从刚才结束的位置(当前位置)往后移3位
long newPositionInStream = memoryStream.CanSeek ? memoryStream.Seek(3, SeekOrigin.Current) : 0;
Console.WriteLine($"重新定位后 Stream.Position 在第{newPositionInStream + 1}位置");
if (newPositionInStream < buffer.Length)
{
//将从新位置一直写到 buffer 的末尾,此时需注意,memoryStream 已经写入了10个数据“Stream Pra”
memoryStream.Write(buffer, (int)newPositionInStream, buffer.Length - (int)newPositionInStream);
}
//写完后将 memoryStream 的 Position 属性设置为0,开始读取流中的数据
memoryStream.Position = 0;
//设置一个空盒子来接收流中的数据,长度由 memoryStream 的长度决定
readBuffer = new byte[memoryStream.Length];
//设置 memoryStream 总的读取数量
//注意,这时候流已经把数据读到了 readBuffer 中
int count = memoryStream.CanRead ? memoryStream.Read(readBuffer, 0, readBuffer.Length) : 0;
//由于我们刚开始使用加密的 Encoding 方式,所以我们需要解密,将 readBuffer 中的数据转化成 char 数组,随后才能重新拼接成 string
//首先,我们先将从流中都回来的数据 readBuffer 转化成相应的 char 数组
int charCount = Encoding.Default.GetCharCount(readBuffer, 0, count);
//通过 char 数量,设定一个新的 readCharBuffer 数组
readCharBuffer = new char[charCount];
//Encoding 类的强悍之处就是不仅包含加密的方法,甚至将解密者都能创建出来(GetDecoder()),解密者便会将 readCharBuffer 填充
//通过 GetChars 方法,把 readBuffer 中 byte 数据逐个转化成 char ,并且按一致顺序填充到 readCharBuffer 中
Encoding.Default.GetDecoder().GetChars(readBuffer, 0, count, readCharBuffer, 0);
for (int i = 0; i < readCharBuffer.Length; i++)
{
newMessageString += readCharBuffer[i];
}
Console.WriteLine($"读取的新字符串为:{newMessageString}");
}
}
Console.ReadLine();
}
catch (Exception ex)
{
throw new Exception(ex.ToString());
}
}
显示结果:

特别注意:memoryStream.Position 这个属性,在复杂的程序中,流对象的操作也会很复杂,一定要将 memoryStream.Position 设置在所需要的正确位置
如上:将
memoryStream.Position = 0 改成 memoryStream.Position = 3
运行程序得出的结果是:

其次,using 语句结束前,会自动销毁 memoryStream 对象,相当于 memoryStream.Close()。
接下来学习下关于流中怎么实现异步操作
在 Stream 基类中有几个关键方法,他们能够很好的实现异步的读写
//异步读取 public virtual IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
//结束异步读取 public virtual int EndRead(IAsyncResult asyncResult) //异步写入 public virtual IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) //结束异步写入 public virtual void EndWrite(IAsyncResult asyncResult)
大家很容易就会发现,异步写入和异步读取两个方法实现的 IAsyncResult 接口,结束异步读取和写入方法中也顺应带上的 IAsyncResult 参数,其实使用并不复杂,特别需要注意的是:
每次调用 Begin 方法时,都必须调用一次相对应的 End 方法
static void Main(string[] args)
{
try
{
byte[] readBuffer_1 = null;
char[] readCharBuffer_1 = null;
string newMessageString_1 = string.Empty;
using (MemoryStream memoryStreamAsync = new MemoryStream())
{
string messageString = "Stream Practice!";
Console.WriteLine($"初始字符串:{messageString}");
if (memoryStreamAsync.CanWrite)
{
//通过 Encoding 实现 string -> byte[] 的转换
byte[] bufferAsync = Encoding.Default.GetBytes(messageString);
//使用异步方法将前10位字符写入,“Stream Pra”
memoryStreamAsync.BeginWrite(bufferAsync, 0, 10, new AsyncCallback(x =>
{
memoryStreamAsync.EndWrite(x);
}), memoryStreamAsync);
Task.Delay(1000).Wait();//保证数据完成写入操作
Console.WriteLine($"现在 Stream.Position 在第{memoryStreamAsync.Position + 1}位置");
//从刚才结束的位置(当前位置)往后移3位
var newPositionStreamAsync = memoryStreamAsync.CanSeek ? memoryStreamAsync.Seek(3, SeekOrigin.Current) : 0;
Console.WriteLine($"重新定位后 Stream.Position 在第{newPositionStreamAsync + 1}位置");
//将从新位置一直写到 bufferAsync 的末尾,此时需注意,memoryStreamAsync 已经写入了10个数据“Stream Pra”
memoryStreamAsync.BeginWrite(bufferAsync, (int)newPositionStreamAsync, bufferAsync.Length - (int)newPositionStreamAsync, new AsyncCallback(x =>
{
if(newPositionStreamAsync > bufferAsync.Length)
memoryStreamAsync.EndWrite(x);
}), memoryStreamAsync);
Task.Delay(1000).Wait();//保证数据完成写入操作
//写完后将 memoryStreamAsync 的 Position 属性设置为0,开始读取流中的数据
memoryStreamAsync.Position = 0;
//设置一个空盒子来接收流中的数据,长度由 memoryStreamAsync 的长度决定
readBuffer_1 = new byte[memoryStreamAsync.Length];
//设置 memoryStreamAsync 总的读取数量
//注意,这时候流已经把数据读到了 readBuffer_1 中
var rc = memoryStreamAsync.CanRead ? memoryStreamAsync.ReadAsync(readBuffer_1, 0, readBuffer_1.Length) : null;
int charCountAsync = Encoding.Default.GetCharCount(readBuffer_1, 0, rc.Result);
readCharBuffer_1 = new char[charCountAsync];
Encoding.Default.GetDecoder().GetChars(readBuffer_1, 0, charCountAsync, readCharBuffer_1, 0);
for(int i = 0; i < readCharBuffer_1.Length; i++)
{
newMessageString_1 += readCharBuffer_1[i];
}
Console.WriteLine($"读取的新字符串为:{newMessageString_1}");
//异步读取 EnginRead() 无法执行
//var rc1 = memoryStreamAsync.CanRead?memoryStreamAsync.BeginRead(readBuffer_1,0,readBuffer_1.Length,x =>
//{
// int countAsync = memoryStreamAsync.EndRead(x);
// if (countAsync > 0)
// {
// int charCountAsync_1 = Encoding.Default.GetCharCount(readBuffer_1, 0, countAsync);
// readCharBuffer_1 = new char[charCountAsync];
// Encoding.Default.GetDecoder().GetChars(readBuffer_1, 0, countAsync, readCharBuffer_1, 0);
// for (int i = 0; i < readCharBuffer_1.Length; i++)
// {
// newMessageString_1 += readCharBuffer_1[i];
// }
// Console.WriteLine($"读取的新字符串为:{newMessageString_1}");
// }
//}, memoryStreamAsync) : null;
}
}
Console.ReadLine();
}
catch (Exception ex)
{
throw new Exception(ex.ToString());
}
}
此处异步读取数据使用的是 public Task<int> ReadAsync(byte[] buffer, int offset, int count) 方法,注释部分无法执行读取操作(暂时不知道原因)
本章总结:
本章介绍了流的基本概念和C#中关于流的基类 Stream 所包含的一些重要的属性和方法,主要是一些属性和方法的细节和我们操作时必须注意的事项
遗留问题:
本章遗留问题主要是异步读取数据方法 BeginRead() 无法执行,该问题暂时无法解答,各位读友可帮忙解惑下
下一章将会介绍操作流类的工具:StreamWriter 和 StreamReader
敬请期待~

浙公网安备 33010602011771号