自由学习记录(28)
C# 中的流(Stream
)
流(Stream
)是用于读取和写入数据的抽象基类。
流表示从数据源读取或向数据源写入数据的矢量过程。
C# 中的流类是从 System.IO.Stream
基类派生的,提供了多种具体实现,每种实现都针对不同的数据源或用途。下面是一些常见的流类型及其作用:
流类型一般都被整合成了一个对象既管输入流 也管输出流,基础概念中的Stream也是如此,只不过有输入输出两个细分的类概念而已
1. 流的基础概念
- 流(Stream):在计算机中表示一种数据的连续流动,可以是从文件、内存、网络或其他设备中读取数据,或者将数据写入这些设备。
- 输入流(InputStream):用于从数据源读取数据。
- 输出流(OutputStream):用于将数据写入到数据源。
2. 常见的流类型
- FileStream:用于从文件读取或向文件写入数据。
- MemoryStream:用于在内存中读写数据,适用于不需要持久化的临时数据处理。
- NetworkStream:用于网络通信,处理通过网络传输的数据。
- BufferedStream:为其他流提供缓冲,以提高读取和写入性能。
- StreamReader 和 StreamWriter:用于处理文本数据的读写操作,通常与其他流(如
FileStream
)一起使用。 - CryptoStream:用于加密和解密数据流。
- GZipStream 和 DeflateStream:用于压缩和解压缩数据流。
3. 内存流(MemoryStream)
- 作用:
MemoryStream
是一个特殊的流,它将数据存储在内存中,而不是在磁盘上或其他外部设备上。适用于处理需要快速读写的临时数据。 - 特点:
- 读写速度快,因为数据存储在内存中。
- 用于在应用程序内部传输数据或在不需要长期存储数据时使用。
- 可以使用
ToArray()
方法将内存流中的数据转换为字节数组。
- 示例用法:
using System; using System.IO; class Program { static void Main() { // 创建一个内存流 using (MemoryStream memoryStream = new MemoryStream()) { // 写入数据到内存流 byte[] data = new byte[] { 1, 2, 3, 4, 5 }; memoryStream.Write(data, 0, data.Length); // 读取数据 memoryStream.Position = 0; // 重置位置,以便读取 byte[] readData = new byte[data.Length]; memoryStream.Read(readData, 0, readData.Length); Console.WriteLine("Read data: " + string.Join(", ", readData)); } } }
4. 文件流(FileStream)
- 作用:
FileStream
是用于从文件读取或向文件写入数据的流。它可以处理大文件的读写操作,并支持同步和异步操作。 - 特点:
- 适用于需要持久化存储数据的场景。
- 可以通过
FileStream
进行大容量文件的读写操作。 - 支持指定读取和写入的起始位置及长度。
- 示例用法:
using System; using System.IO; class Program { static void Main() { // 创建或打开文件进行写入 using (FileStream fileStream = new FileStream("example.txt", FileMode.Create)) { byte[] data = new byte[] { 1, 2, 3, 4, 5 }; fileStream.Write(data, 0, data.Length); } // 读取文件数据 using (FileStream fileStream = new FileStream("example.txt", FileMode.Open)) { byte[] readData = new byte[fileStream.Length]; fileStream.Read(readData, 0, readData.Length); Console.WriteLine("Read data from file: " + string.Join(", ", readData)); } } }
5. 内存流与文件流的角色
- 内存流:适用于快速存储和处理临时数据。由于数据存在内存中,操作速度较快。适合数据量较小、需要在程序运行期间使用的数据(如缓存)。
- 文件流:用于处理需要持久化存储的数据,或者需要对较大文件进行读写的场景。它适合从文件中读取数据或将数据写入到文件的情况。
6. 内存流与文件流的比较
- 速度:内存流速度较快,因为数据在内存中。文件流通常慢一些,因为数据需要通过磁盘进行读写。
- 存储:内存流的数据是临时的,只在内存中存在。文件流的数据持久化存储在磁盘中。
- 使用场景:
- 内存流:适用于处理小到中等大小的数据或需要快速操作的数据,如临时数据的缓存。
- 文件流:适用于处理大文件或需要长期存储的文件,如日志文件、文档、数据库文件等。
总结
- 内存流(
MemoryStream
):在内存中读写数据,适合处理临时数据和高效的数据操作。 - 文件流(
FileStream
):用于文件读写,适合需要持久化存储的场景,支持大文件处理。
内存流和序列化
MemoryStream
内存中读写数据,一个流对象
序列化 对象转换为可以存储或传输的格式
结合这两个概念,你可以将对象序列化为字节流,并在内存中操作它们。
以下是一些常见的方法来实现内存流序列化:
1. BinaryFormatter
二进制序列化
.NET 中一个常用的序列化工具,可将对象转换为二进制格式,并将其写入流中。
示例代码:
using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
[Serializable]
public class MyClass
{
public string Name { get; set; }
public int Age { get; set; }
}
class Program
{
static void Main()
{
// 创建对象
MyClass obj = new MyClass { Name = "Alice", Age = 30 };
// 将对象序列化到内存流中
using (MemoryStream memoryStream = new MemoryStream())
{
IFormatter formatter = new BinaryFormatter();
formatter.Serialize(memoryStream, obj);
// 将内存流的字节数组用于其他操作,例如保存到文件或传输
byte[] serializedData = memoryStream.ToArray();
// 反序列化对象
using (MemoryStream readStream = new MemoryStream(serializedData))
{
MyClass deserializedObj = (MyClass)formatter.Deserialize(readStream);
Console.WriteLine($"Name: {deserializedObj.Name}, Age: {deserializedObj.Age}");
}
}
}
}
注意:BinaryFormatter
已被标记为不推荐使用,因为它存在安全风险,特别是在处理不受信任的数据时。建议使用其他序列化方法,如 System.Text.Json
或 XmlSerializer
。
2. 使用 JsonSerializer
进行 JSON 序列化
如果你希望使用更现代且安全的序列化格式,可以使用 System.Text.Json.JsonSerializer
。
示例代码:
using System;
using System.IO;
using System.Text.Json;
public class MyClass
{
public string Name { get; set; }
public int Age { get; set; }
}
class Program
{
static void Main()
{
// 创建对象
MyClass obj = new MyClass { Name = "Alice", Age = 30 };
// 将对象序列化为 JSON 格式
using (MemoryStream memoryStream = new MemoryStream())
{
JsonSerializer.Serialize(memoryStream, obj);
memoryStream.Position = 0; // 重置流的位置,以便读取数据
// 反序列化对象
MyClass deserializedObj = JsonSerializer.Deserialize<MyClass>(memoryStream);
Console.WriteLine($"Name: {deserializedObj.Name}, Age: {deserializedObj.Age}");
}
}
}
3. 使用 XmlSerializer
进行 XML 序列化
如果你需要序列化为 XML 格式,可以使用 XmlSerializer
。
示例代码:
using System;
using System.IO;
using System.Xml.Serialization;
public class MyClass
{
public string Name { get; set; }
public int Age { get; set; }
}
class Program
{
static void Main()
{
// 创建对象
MyClass obj = new MyClass { Name = "Alice", Age = 30 };
// 将对象序列化为 XML 格式
using (MemoryStream memoryStream = new MemoryStream())
{
XmlSerializer serializer = new XmlSerializer(typeof(MyClass));
serializer.Serialize(memoryStream, obj);
memoryStream.Position = 0; // 重置流的位置,以便读取数据
// 反序列化对象
MyClass deserializedObj = (MyClass)serializer.Deserialize(memoryStream);
Console.WriteLine($"Name: {deserializedObj.Name}, Age: {deserializedObj.Age}");
}
}
}
总结
- 使用
BinaryFormatter
进行二进制序列化和反序列化。 - 使用
System.Text.Json.JsonSerializer
进行 JSON 格式的序列化和反序列化,推荐用于现代应用。 - 使用
XmlSerializer
进行 XML 格式的序列化和反序列化。
在选择序列化方法时,请根据你的需求和数据的安全性考虑。BinaryFormatter
不推荐用于处理不可信数据,而 System.Text.Json
和 XmlSerializer
更加安全和现代。
特性(Attributes)
每一个特性实例都是贴在挂了[] 的元素
这个元素可以在运行时通过反射调用特性类实例中的各种成员和方法
这些方法又可以反过来作用于挂[]的元素上,
比如特性挂在了某个类的方法上,那么每次调用这个方法的话
都是经过了挂在这个方法上的特性类实例的处理后,再产生最终结果的
不严谨,有错误的理解,但大致思路是这样的,只是这个处理应该不是在运行时动态的,还会扯到框架什么什么的,只不过目前的水平还深入不下去,目前这样理解也勉强凑合了
自己定义了一个特性类,这个特性类只要被挂在任意的一个元素上面,就会生成一个与这个元素相关联的特性类实例
类和方法的特性互不影响
-
类上的特性 只会影响类本身,与类中的方法、属性无关。
-
方法上的特性 只会影响该方法,与类的其他成员无关。
一个元素可以有多个特性。在 C# 中,多个特性可以作用在同一个类、方法、属性等元素上,每个特性会独立存在,且互不影响。
一个继承了 Attribute
的类可以被写成 []
中的特性语法,并附加到方法、属性、类等代码元素上
当你将特性应用到代码元素时,编译器会将特性实例的信息记录到元数据中,而不会直接在运行时创建该特性实例。
自建特性类的特性AttributeUsage
的参数
-
AttributeTargets
:指定可以应用特性的位置(类、方法、属性等)。 -
AllowMultiple
:是否允许同一代码元素上应用多个实例。 -
Inherited
:是否允许特性从基类继承。
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public class MyCustomAttribute : Attribute
{
public string Info { get; }
public MyCustomAttribute(string info)
{
Info = info;
}
}
特性是通过反射获取的,不会直接附加到代码元素实例上。
加上特性之后,只有通过反射机制才能获得该特性的实例。
Type type = typeof(ExampleClass);
var attributes = type.GetCustomAttributes(typeof(MyCustomAttribute), false);
if (attributes.Length > 0)
{
// 特性实例
MyCustomAttribute myAttr = (MyCustomAttribute)attributes[0];
Console.WriteLine($"Name: {myAttr.Name}, Version: {myAttr.Version}");
}
特性(Attributes) ,元编程机制,向代码元素(如类、方法、属性等)附加元数据。
这种元数据可以在运行时通过反射读取,并驱动额外的逻辑行为。
特性的原理
-
特性是类的实例
-
特性本质上是从
System.Attribute
派生的类
-
-
特性的声明方式:
-
特性类必须继承自
System.Attribute
。 -
特性通常通过
[AttributeUsage]
特性限制它可以应用的位置和次数。 -
特性可以有构造函数和属性,用于设置参数和配置。
-
特性如何工作
1. 编译时:将元数据嵌入到程序集
当你在代码中添加一个特性时,例如:
[Serializable]
public class MyClass { }
编译器会在程序集的元数据中为 MyClass
记录 Serializable
特性。
2. 运行时:通过反射读取特性
你可以在运行时通过反射读取特性的信息,并基于这些信息执行特定操作。
特性的创建与使用
1. 自定义特性
以下是自定义特性的示例:
using System;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public class MyCustomAttribute : Attribute
{
public string Name { get; }
public int Version { get; }
public MyCustomAttribute(string name, int version)
{
Name = name;
Version = version;
}
}
2. 应用特性
将特性应用到类或方法上:
[MyCustomAttribute("ExampleClass", 1)]
public class Example
{
[MyCustomAttribute("ExampleMethod", 2)]
public void TestMethod()
{
Console.WriteLine("Hello from TestMethod");
}
}
3. 读取特性
通过反射读取特性的信息:
using System;
using System.Reflection;
class Program
{
static void Main()
{
// 获取类的特性
Type type = typeof(Example);
var classAttributes = type.GetCustomAttributes<MyCustomAttribute>();
foreach (var attr in classAttributes)
{
Console.WriteLine($"Class: {type.Name}, Name: {attr.Name}, Version: {attr.Version}");
}
// 获取方法的特性
MethodInfo method = type.GetMethod("TestMethod");
var methodAttributes = method.GetCustomAttributes<MyCustomAttribute>();
foreach (var attr in methodAttributes)
{
Console.WriteLine($"Method: {method.Name}, Name: {attr.Name}, Version: {attr.Version}");
}
}
}
输出:
Class: Example, Name: ExampleClass, Version: 1
Method: TestMethod, Name: ExampleMethod, Version: 2
为什么特性可以实现“奇奇怪怪”的功能?
1. 编译器的内置支持
许多特性(如 [Serializable]
, [Obsolete]
)在编译器中被特殊处理:
[Serializable]
:提示编译器生成支持序列化的代码。[Obsolete]
:在编译时发出警告或错误,提醒使用者该代码已经过时。
2. 框架和库通过特性扩展功能
特性可以驱动框架的运行行为。例如:
- ASP.NET Core 使用特性(如
[HttpGet]
,[HttpPost]
)标记控制器的路由规则。 - Entity Framework 使用特性(如
[Key]
,[Required]
)定义数据库表的映射规则。 - JSON 序列化库 使用特性(如
[JsonIgnore]
,[JsonProperty]
)控制对象的序列化行为。
3. 特性允许插入元编程逻辑
特性是实现 AOP(面向切面编程) 的核心工具,可以在特定代码执行时插入行为。例如:
[Log]
特性可以自动记录方法的调用信息。[Validate]
特性可以在方法调用前校验参数。
总结:特性的工作流程
- 声明特性:通过继承
System.Attribute
定义自定义特性。 - 应用特性:将特性以
[AttributeName]
的形式附加到代码元素上。 - 元数据存储:编译器将特性信息存储在程序集的元数据中。
- 读取特性:运行时通过反射获取元数据,执行逻辑。
特性看似只是加了个 []
,实际上背后依赖了 C# 的强大编译器支持和运行时反射机制,使得它能够驱动复杂的逻辑,成为元编程的重要工具。
Encoding.UTF8.GetString()
将字节数组(byte[]
)解码为字符串。
两种重载
1. 方法签名
重载1:直接传入字节数组
public string GetString(byte[] bytes);
重载2:指定范围
public string GetString(byte[] bytes, int index, int count);
- 参数说明:
byte[] bytes
: 要解码的字节数组。int index
: 哪个索引开始解码。int count
: 解码的字节数。
2. 用法示例
解码整个字节数组
using System;
using System.Text;
class Program
{
static void Main()
{
byte[] utf8Bytes = { 72, 101, 108, 108, 111 }; // "Hello" 的 UTF-8 编码
string result = Encoding.UTF8.GetString(utf8Bytes);
Console.WriteLine(result); // 输出: Hello
}
}
解码部分字节
using System;
using System.Text;
class Program
{
static void Main()
{
byte[] utf8Bytes = { 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100 }; // "Hello World" 的 UTF-8 编码
// 解码从索引 6 开始的 5 个字节
string result = Encoding.UTF8.GetString(utf8Bytes, 6, 5);
Console.WriteLine(result); // 输出: World
}
}
3. 参数的关键点
参数合法性
- 字节数组 (
byte[]
) 必须是合法的 UTF-8 数据:- 数组中包含非法或不完整的 UTF-8 字节序列,
GetString
会抛出异常或替换为 Unicode 替代字符(通常是�
)。
- 数组中包含非法或不完整的 UTF-8 字节序列,
- 索引和长度合法性:
index
和count
必须满足:index >= 0
count >= 0
index + count <= bytes.Length
- 否则会抛出
ArgumentOutOfRangeException
。
常用场景
- 解码整个字节数组:
Encoding.UTF8.GetString(bytes)
- 解码部分数据(如网络流数据或文件流数据的片段):
Encoding.UTF8.GetString(bytes, index, count)
4. 为什么有两种重载?
- 第一种重载适合解码完整的字节数组,简单直接。
- 第二种重载提供灵活性,允许你仅解码字节数组的一部分,这在处理大文件或流式数据时非常有用。
5. 总结
- 第一种重载(整个字节数组):
GetString(byte[] bytes)
解码完整数组,常见于静态数据的解码。 - 第二种重载(部分字节数组):
GetString(byte[] bytes, int index, int count)
解码字节数组的指定部分,适合处理流式数据或分块操作。
fs.Write()
FileStream
类提供,将数据写入文件。
文件流基于字节bytes
所有参数都是必需的,并且没有提供默认值
参数定义了写入的数据以及写入的范围,具体如下:
public override void Write(byte[] array, int offset, int count);
-
byte[] array
:写入文件的数据 -
int offset
:写入数据的起始索引(array[offset]
开始写入) -
int count
:- 指定
bytes数组
中写入的字节数。 - 这个参数可以限制实际写入的数据长度,即使字节数组长度大于
count
,也只会写入指定数量的字节。
- 指定
示例代码
using System;
using System.IO;
class Program
{
static void Main()
{
// 创建字节数据
byte[] data = new byte[] { 65, 66, 67, 68, 69 }; // 对应 ASCII 字符 A, B, C, D, E
// 打开文件流(会覆盖现有内容)
using (FileStream fs = new FileStream("example.txt", FileMode.Create, FileAccess.Write))
{
// 写入 data 数组中的前 3 个字节(A, B, C)
fs.Write(data, 0, 3);
}
Console.WriteLine("写入完成!");
}
}
输出到 example.txt
的内容:
ABC
关键点补充
为什么不提供默认参数?
设计上,fs.Write()
是低级别的 I/O 方法,用于操作任意二进制数据。它的三个参数控制了写入的范围和数据来源,以下是设计理念:
- 灵活性:
offset
和count
允许你仅写入字节数组的一部分,而不是整个数组。 - 效率:你可以通过精确控制写入范围来优化性能,避免不必要的字节拷贝。
- 防止错误:如果默认写入整个数组,可能导致意外写入多余数据。
如何简化操作?
大多数常见的用法可以显式写成:fs.Write(data, 0, data.Length);
使用 BinaryWriter
BinaryWriter
是一个更高层的封装,简化了写入数据的操作,无需明确指定 offset
和 count
。它会直接将完整的字节数组写入文件:
using (var fs = new FileStream("example.txt", FileMode.Create, FileAccess.Write))
using (var writer = new BinaryWriter(fs))
{
byte[] data = new byte[] { 65, 66, 67 }; // ABC
writer.Write(data); // 自动写入整个数组
}
使用 StreamWriter
(适用于文本数据)
如果处理的是文本而不是二进制数据,StreamWriter
更适合,它完全隐藏了底层字节操作:
using (var writer = new StreamWriter("example.txt"))
{
writer.Write("Hello, World!"); // 写入文本
}
byte[] array
- 文件流操作是以字节为单位,需要将任何数据(如文本、数字、图像)转换为字节数组。
- 常见的转换方法:
- 字符串 转换为字节数组:
Encoding.UTF8.GetBytes(string)
- 数值类型 转换为字节数组:
BitConverter.GetBytes(int)
- 字符串 转换为字节数组:
offset
和 count
- 通过
offset
和count
参数可以选择性地从字节数组中提取一部分数据写入文件。 - 示例:
byte[] data = { 65, 66, 67, 68, 69 }; // A, B, C, D, E fs.Write(data, 2, 2); // 从索引 2 开始写入 2 个字节 -> 写入 "CD"
性能优化
- 数据量较大,使用
fs.Write()
尽量批量写入而不是逐字节写入 - 能减少 I/O 操作次数,提升性能
常见错误
-
数组越界:
- 如果
offset + count > array.Length
,会抛出ArgumentException
。 - 在使用前检查数组长度以避免错误:
if (offset + count > data.Length) throw new ArgumentException("指定的范围超出了数组长度!");
- 如果
-
流未打开或已关闭:
- 如果文件流在调用
Write()
时已关闭,ObjectDisposedException
会被抛出。
- 如果文件流在调用
-
权限不足:
- 如果文件流是只读模式(
FileAccess.Read
),会抛出NotSupportedException
。
- 如果文件流是只读模式(
移动会把文件夹里的所有东西一起移动过去
File.ReadAllLines()
File.ReadAllText()
File.Delete()
File.Copy()
File.Replace()
Application.dataPath