自学内容网 自学内容网

【unity小技巧】unity/C#对文件文件夹的操作,转换二进制数据,并实现持久化读取存储二进制数据,对C#类对象进行二进制序列化和反序列化并存储读取

文章目录

前言

1、为什么要学习二进制读写数据?

之前我已经写过了实现对Json数据的序列化和反序列化持久化存储:
【推荐100个unity插件之37】unity使用三种方式实现对Json数据的序列化和反序列化持久化存储,并举例分析JsonUtlity、LitJSON和Newtonsoft的区别

清晰易懂是他的好处,但是也是一把双刃剑,比如如果我们用json存储数据,只要玩家找到对应的存储信息,就能够快速修改其中的内容(当然我们可以对数据进行加密)。

而且由于他把数据转换成了对应的json字符串,我们最终在存储数据时存储的都是字符串数据,在读写时效率较低,内存和硬盘空间也占用较大。

总结:json安全性和效率较低(其实还包括xml文件格式也是)

2、二进制的好处

通过二进制进行数据持久化的好处

  • 安全性较高
  • 内存和硬盘空间占用小
  • 效率较高
  • 为网络通信做铺垫

3、二进制文件读写的本质

它就是通过将各类型变量转换为字节数组,将字节数组直接存储到文件中

一般人是看不懂存储的二进制数据的,不仅可以节约存储空间,提升效率
还可以提升安全性,而且在网络通信中我们直接传输的数据也是字节数据(二进制数据)


一、前置知识

1、使用string.Join进行打印调试

string.Join 是 .NET 中用于连接字符串数组或集合的一个非常实用的方法。它将一个数组或集合中的每个元素连接成一个单一的字符串,并可以指定一个分隔符。

返回连接后的字符串,所有的元素会按照给定的分隔符拼接起来。

比如

string[] words = { "apple", "banana", "cherry" };
string result = string.Join(", ", words);
Debug.Log(result);  // 输出 "apple, banana, cherry"

2、编码格式

编码是用预先规定的方法将文字、数字或其它对象编成数码,或将信息、数据转换成规定的电脉冲信号。

为保证编码的正确性,编码要规范化、标准化即需有标准的编码格式。

常见的编码格式有ASCII、ANSI、GBK、GB2312、UTF~8、GB18O3a和JNICODE等。

计算机中数据的本质其实就是二进制数据,编码格式就是用对应的二进制数对应不同的文字,由于世界上有各种不同的语言,所有会有很多种不同的编码格式,不同的编码格式对应的规则是不同的

如果在读取字符时采用了不统一的编码格式,可能会出现乱码

游戏开发中常用编码格式 UTF-8
中文相关编码格式 GBK
英文相关编码格式 ASCII

3、获取各种文件路径

Application.dataPath

dataPath 是应用程序的根目录路径,用于访问和存储游戏的 Assets 文件夹中的文件。这在编辑器中有效,特别是在开发阶段,但在构建后的应用中,通常不会直接用于存储持久数据。

Application.persistentDataPath

Application.persistentDataPath获取的是一个持久化的数据路径,在不同的操作系统上,unity会为我们分配不同的持久化数据路径,这样可以确保应用程序在不同平台上都能正确保存和访问

//设置存档文件路径
savePath = Path.Combine(Application.persistentDataPath, "saveData.json");

不同平台存储的路径不一样,我们可以打印savePath查看自己的存储路径

比如我PC上的就是
在这里插入图片描述

其他

https://blog.csdn.net/qq_36303853/article/details/129688513

4、BitConverter.ToInt32

BitConverter.ToInt32 是 C# 中的一个方法,用于将字节数组转换为 32 位整数(int)。

public static int ToInt32 (byte[] value, int startIndex);

参数:

  • value:一个字节数组,包含要转换的字节数据。
  • startIndex:要从字节数组开始转换的位置(索引)。即从该索引处开始读取 4 个字节并将它们转换为 int。

5、什么是序列化和反序列化

  • 序列化其实就是把内存中的数据存储到硬盘上
  • 反序列化其实就是把硬盘上的数据读取到内存中

二、各类型数据和字节数据相互转换

非字符各类型数据转换

C#提供了一个公共类帮助我们进行转化,我们只需要记住API即可

  • 类名:BitConverter
  • 命名空间:using System

1、BitConverter.GetBytes()将各类型转成字节数组

byte[] bytes = BitConverter.GetBytes(256);
Debug.Log(string.Join("", bytes));

结果
在这里插入图片描述

2、BitConverter.ToInt32()将字节数组转成各类型

int i = BitConverter.ToInt32(bytes, 0);
print(i);

结果
在这里插入图片描述

字符串类型数据转换

字符串转字节数组通常需要指定字符编码(例如 UTF-8 或 ASCII)。
在C#中有一个专门的编码格式类 来帮助我们将字符串和字节数组进行转换

  • 类名:Encoding
  • 需要引用命名空间:using System.Text;

1.Encoding.xxx.GetBytes()将字符串以指定编码格式转字节

byte[] bytes2 = Encoding.UTF8.GetBytes("向宇的客栈");
Debug.Log(string.Join("", bytes2));

打印结果
在这里插入图片描述

注意,默认情况下,打印字节数组时,C# 会将其显示为 十进制。

如果你想打印出字节的 二进制表示,可以将每个字节转换为二进制字符串,并在打印时进行连接。可以使用 Convert.ToString 方法来实现这一点。(了解一下就行)

byte[] bytes2 = Encoding.UTF8.GetBytes("向宇的客栈");
string binaryString = string.Join(" ", bytes2.Select(b => Convert.ToString(b, 2).PadLeft(8, '0')));
Debug.Log(binaryString);

结果
在这里插入图片描述

2.Encoding.xxx.GetString()字节数组以指定编码格式转字符串

byte[] bytes2 = Encoding.UTF8.GetBytes("向宇的客栈");
string str = Encoding.UTF8.GetString(bytes2);
print(str);

打印结果
在这里插入图片描述


三、文件操作

1、File.Exists判断文件是否存在

if(File.Exists(Application.dataPath + "/Test.txt")){
    print("文件存在");
}else{
    print("文件不存在");
}

2、File.Create创建文件

//创建文件
FileStream fs = File.Create(Application.dataPath + "/Test.txt");

创建了文件,如果想写入内容,记得要先关闭关闭文件流,才允许写入内容

//创建文件
FileStream fs = File.Create(Application.dataPath + "/Test.txt");
//手动关闭文件流
fs.Close();
//写入内容
File.WriteAllText(Application.dataPath + "/Test.txt", "内容2");

ps:虽然我们正常情况下我们不需要这么做,因为File.WriteAllText本身就是直接创建并写入文件内容(下面会介绍),但是这里算是一个补充知识,大家了解就行了


3、写入文件内容

注意

  • 如果文件不存在会新建,并不会报错,但是如果文件夹不存在则会报错
  • 如果

文件已存在,且存在内容,会替换里面的内容

3.1 同步写入文件内容

  • 同步操作:会阻塞当前线程直到写入完成。
  • 适用于小文件或在不需要处理大量I/O操作的情况下。
  • 不适合用于UI线程或需要保持响应的应用,因为它会导致线程被阻塞,影响性能。
3.1.1 创建并替换文件内容
WriteAllBytes将字节数组文件里
byte[] bytes3 = BitConverter.GetBytes(999);
File.WriteAllBytes(Application.dataPath + "/Test1.txt", bytes3);

结果,由于我们将 byte[] 数组(即二进制数据)直接写入文件,而不是将其作为文本数据存储。这会导致在文件中存储的内容并不是你期望的字符,而是原始的二进制数据,因此当你尝试打开文件时,显示的就是乱码。但是你可以先不用管他,有数据就行
在这里插入图片描述

WriteAllLines将指定的string数组内容一行行写入到指定的文件中
string[] strs = new string[]{"123", "内容", "asdasdasfasd123123asdasd"};
File.WriteAllLines(Application.dataPath + "/Test2.txt", strs);

结果,数组的内容会被分成不同段落进行存储
在这里插入图片描述

WriteAllText将指定字符串写入指定路径
File.WriteAllText(Application.dataPath + "/Test3.txt", "内容内容\n下一段内容xcasdas123121");

结果,支持转义字符,比如\n就是换行符
在这里插入图片描述

如果文件夹不存在则会报错

比如下面代码

File.WriteAllText(Application.dataPath + "/data/Test4.txt", "内容");

项目里我并没有新建data文件夹,结果就报错了
在这里插入图片描述
改成下面这样就可以了

//创建文件夹
Directory.CreateDirectory(Application.dataPath + "/data");
//写入内容
File.WriteAllText(Application.dataPath + "/data/Test4.txt", "内容");
3.1.2 创建并追加内容到现有文件中
将指定的string数组内容一行行追加到指定的文件中
string[] strs2 = new string[]{"追", "加", "内容"};
File.AppendAllLines(Application.dataPath + "/Test2.txt", strs2);

结果
在这里插入图片描述

将指定字符串追加到指定的文件中
File.AppendAllText(Application.dataPath + "/Test3.txt", "追加的内容");

结果
在这里插入图片描述

将字节数组追加到指定的文件中

你可能在等一个File.AppendAllBytes方法,但是很可惜,并没有这个方法

原因:

  • File.AppendAllBytes 方法没有被直接实现,可能是因为追加字节数组到文件的操作本身不如文本的追加操作那么常见或广泛使用。
  • 对于二进制数据的追加,可以通过 FileStream 直接以追加模式来实现,这样更灵活且适用于大多数情况。
  • 另一方面,文本数据的追加(例如文本文件、日志文件等)是比较常见的需求,所以 File.AppendAllText 和 File.AppendAllLines 这类方法便于直接处理文本的追加。

至于如何使用 FileStream 来追加字节数据,我后面再说。

注意
  • 如果文件不存在会新建,并不会报错,但是如果文件夹不存在则会报错
  • 如果文件已存在,且存在内容,会追加内容到现有文件中

3.2 异步操作文件内容

  • 异步操作:不会阻塞当前线程,可以在后台完成文件写入,允许UI线程或其他任务继续工作。
  • 适用于需要执行大量文件写入操作的应用,特别是那些在UI线程上运行的应用程序(如桌面应用或Web应用),可以避免卡顿或降低响应性。
  • 异步方法通常能提高程序的性能,尤其是在处理较大的文件或需要频繁I/O操作时。

使用和前面同步类似,就不单独介绍了

3.2.1 创建并替换文件内容
//将指定的字节数组 写入到指定的路径文件里
byte[] bytes3 = BitConverter.GetBytes(999);
File.WriteAllBytesAsync(Application.dataPath + "/Test1.txt", bytes3);

//将指定的string数组内容 一行行写入到指定的路径中
string[] strs = new string[]{"123", "内容", "asdasdasfasd123123asdasd"};
File.WriteAllLinesAsync(Application.dataPath + "/Test2.txt", strs);

//将指定字符串写入指定路径
File.WriteAllTextAsync(Application.dataPath + "/Test3.txt", "内容");
3.2.2 创建并追加内容到现有文件中
//将指定的string数组内容 一行行追加到指定的路径中
string[] strs2 = new string[]{"追", "加", "内容"};
File.AppendAllLinesAsync(Application.dataPath + "/Test3.txt",strs2);

//将指定字符串追加到指定路径
File.AppendAllLinesAsync(Application.dataPath + "/Test3.txt",strs2);
3.2.3 await 关键字用于等待异步操作完成
没有 await 时:

如果你不使用 await,那么 File.WriteAllTextAsync 会立即启动异步操作,返回一个 Task 对象,而后续的 Console.WriteLine(“文件写入完成!”) 会马上执行,不管文件写入是否完成。

比如:

public async Task WriteDataAsync()
{
    string text = "Hello, World!";
    File.WriteAllTextAsync("path/to/file.txt", text);  // 异步写入
    Console.WriteLine("文件写入完成!");  // 立即执行
}

这样做的问题是,WriteLine 可能在文件写入之前就已经执行了,导致你看到文件写入还没完成,但程序已经打印出完成的提示。

使用 await 时:

使用 await 后,程序会暂停执行,直到 WriteAllTextAsync 完成,才会继续执行 Console.WriteLine(“文件写入完成!”)。

比如:

public async Task WriteDataAsync()
{
    string text = "Hello, World!";
    await File.WriteAllTextAsync("path/to/file.txt", text);  // 等待文件写入完成
    Console.WriteLine("文件写入完成!");  // 只有在文件写入后才执行
}

这样可以确保文件写入操作完成后,再打印完成提示。


4、读取文件内容

注意
注意:如果读取的文件不存在会报错

4.1 同步读取文件

4.1.1 ReadAllBytes读取字节数据
byte[] bytes = File.ReadAllBytes(Application.dataPath + "/Test1.txt");
print(string.Join("", bytes));
print(BitConverter.ToInt32(bytes, 0));

打印结果
在这里插入图片描述

4.1.2 ReadAllLines读取所有行信息
string[] strs = File.ReadAllLines(Application.dataPath + "/Test2.txt");
print(string.Join(", ", strs));

打印结果
在这里插入图片描述

4.1.3 ReadAllText读取所有文本信息
string str = File.ReadAllText(Application.dataPath + "/Test3.txt");
print(string.Join(", ", str));

打印结果
在这里插入图片描述

4.2 异步读取文件

用法类似,这里直接举个例子就行了,不过多介绍

using System;
using System.IO;
using System.Threading.Tasks;
using UnityEngine;

public class Test : MonoBehaviour
{
    async void Start()
    {
        await Log();
    }

    public async Task Log()
    {
 try
        {
            //读取字节数据
            byte[] bytes = await File.ReadAllBytesAsync(Application.dataPath + "/Test1.txt");
            print(string.Join("", bytes));
            print(BitConverter.ToInt32(bytes, 0));

            //读取所有行信息
            string[] strs = await File.ReadAllLinesAsync(Application.dataPath + "/Test2.txt");
            print(string.Join(", ", strs));

            //读取所有文本信息
            string str = await File.ReadAllTextAsync(Application.dataPath + "/Test3.txt");
            print(string.Join(", ", str));
        }
        catch (Exception ex)
        {
            Console.WriteLine($"发生错误: {ex.Message}");
        }
    }
}

结果,和同步前面一样
在这里插入图片描述

5、删除文件

File.Delete(Application.dataPath + "/Test1.txt");

如果删除的文件路径,文件夹都不存在,则会报错,比如File.Delete(Application.dataPath + "/data/Test1.txt");,我项目里并没有data文件夹,运行则会报错
在这里插入图片描述

注意

  • 如果删除打开着的文件 会报错
  • 如果删除的文件不存在什么都不做,不会报错
  • 但是如果删除的文件路径,文件夹都不存在,则会报错

6、复制文件

File.Copy 方法是 .NET 中用于复制文件的方法。

  • 参数一:要复制的源文件的路径(包括文件名)
  • 参数二:目标文件的路径(包括文件名)
  • 参数三(可选):一个布尔值,指定是否允许覆盖目标文件。
    • true: 如果目标文件已存在,则覆盖目标文件。
    • false: 默认值,如果目标文件已存在,则抛出 IOException 异常。
File.Copy(Application.dataPath + "/Test3.txt", Application.dataPath + "/Test4.txt", true);

注意:

  • 如果要复制的源文件的路径不存在会报错
  • 要复制的源文件需要是文件流关闭状态

7、将一个文件替换为另一个文件,并允许在替换操作时进行备份

File.Replace 是 .NET 中的一个方法,用于将一个文件替换为另一个文件,并且可以选择是否保留旧文件的备份。

  • 参数一:要替换的源文件的路径(包括文件名)。这个文件将用来替换目标文件。
  • 参数二:目标文件的路径(包括文件名)。这是将要被替换的文件。
  • 参数三:备份文件的路径(包括文件名)。这是在替换操作完成之前对目标文件进行备份的位置。如果不想创建备份,可以传入 null。
  • 参数四(可选):默认值是 false。为 true 忽略替换文件中的合并错误(如属性和访问控制列表(ACL)到替换文件;

比如目前存在test2 test3文件信息如下
在这里插入图片描述
在这里插入图片描述

File.Replace(Application.dataPath + "/Test3.txt", Application.dataPath + "/Test2.txt", Application.dataPath + "/backupfile.txt", true);

结果,Test2里的内容替换了成了Test3里的内容,生成新的backupfile为Test2的内容备份文件
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

注意

  • 如果要替换的源文件或目标文件不存在都会报错
  • 要替换的源文件和目标文件都需要是文件流关闭状态

五、使用文件流操作文件

1、什么是文件流?

在c#中提供了一个文件流类FileStream类,它主要作用是用于读写文件的细节,我们之前学过的File只能整体读写文件,而FileStream可以以读写字节的形式处理文件

说人话就是:

文件里面存储的数据就像是一条数据流(数组或者列表),我们可以通过FileStream一部分一部分的读写数据流,比如我可以先存一个int(4个字节) 再存一个bool(1个字节) 再存一个string(n个字节),利用FileStream可以以流式逐个读写

2、FileStream常用方法

new FileStream用于读取和写入文件内容

  • 参数一:指定要打开或创建的文件的路径。如果文件不存在,会根据 FileMode 的不同进行处理。
  • 参数二:一个 FileMode 枚举值,表示打开文件的方式。常见的值有:
    • FileMode.Open:如果文件存在,则打开它;如果文件不存在,则抛出异常。
    • FileMode.Create:如果文件不存在,则创建它;如果文件存在,则覆盖它。
    • FileMode.Append:打开文件,如果文件不存在,则创建它;如果文件存在,则追加内容。
    • FileMode.OpenOrCreate:如果文件存在,则打开它;如果文件不存在,则创建它。
    • FileMode.Truncate:打开文件并将其长度截断为零(即清空文件内容)。
    • FileMode.CreateNew:如果文件已存在,则抛出异常;如果文件不存在,则创建它。
  • 参数三(可选):一个 FileAccess 枚举值,指定文件的访问权限。默认值是 FileAccess.ReadWrite,常见的值有:
    • FileAccess.Read:只允许读取文件。
    • FileAccess.Write:只允许写入文件。
    • FileAccess.ReadWrite:允许读写文件。
  • 参数四(可选):一个 FileShare 枚举值,指定其他进程如何共享该文件。默认值是 FileShare.None,常见的值有:
    • FileShare.Read:允许其他进程读取文件。
    • FileShare.Write:允许其他进程写入文件。
    • FileShare.ReadWrite:允许其他进程读取和写入文件。
    • FileShare.None:不允许其他进程对该文件进行任何共享操作。
    • FileShare.Delete:允许其他程序删除文件。
  • 参数五(可选):指定缓冲区的大小,以字节为单位。默认缓冲区大小是 4096 字节(4KB)。
  • 参数六(可选):指定是否使用异步操作来读取或写入文件。默认为同步操作及false
    • 如果为 true,则文件操作将在后台线程中异步执行。
    • 此参数为 false 时,文件操作将在主线程中同步执行。
FileStream fs = new FileStream(Application.dataPath + "/Test2.txt", FileMode.OpenOrCreate);

File.Create用于创建一个新的文件或覆盖一个已存在的文件

  • 参数一:文件路径,可以是相对路径或绝对路径。如果文件不存在,会根据 FileMode 的不同进行处理。
  • 参数二(可选):指定缓冲区的大小,以字节为单位。默认缓冲区大小是 4096 字节(4KB)。
  • 参数三(可选):指定是否使用异步操作来读取或写入文件。默认为同步操作及false
    • FileOptions.None:默认值,表示不指定任何选项。
    • FileOptions.Asynchronous:指定文件操作是异步的。
    • FileOptions.DeleteOnClose:指定在文件关闭时删除文件。
    • FileOptions.SequentialScan:表示从头到尾顺序扫描访问文件。
    • FileOptions.RandomAccess:表示文件是随机访问的。
    • FileOptions.WriteThrough:指定文件的写入操作应立即写入到磁盘。
    • FileOptions.Encrypted:加密。
FileStream fs = File.Create(Application.dataPath + "/Test2.txt");

File.Open允许对文件进行读写操作

File.Open 是 .NET 中 System.IO.File 类的一个静态方法,用于打开文件并返回一个 FileStream 对象,允许对文件进行读写操作。通过 File.Open,你可以指定文件访问权限、文件模式以及文件共享方式。

  • 参数一:文件的路径,可以是绝对路径或相对路径。如果文件不存在,会根据 FileMode 的不同进行处理。
  • 参数二:一个 FileMode 枚举值,表示打开文件的方式。常见的值有:
    • FileMode.Open:如果文件存在,则打开它;如果文件不存在,则抛出异常。
    • FileMode.Create:如果文件不存在,则创建它;如果文件存在,则覆盖它。
    • FileMode.Append:打开文件,如果文件不存在,则创建它;如果文件存在,则追加内容。
    • FileMode.OpenOrCreate:如果文件存在,则打开它;如果文件不存在,则创建它。
    • FileMode.Truncate:打开文件并将其长度截断为零(即清空文件内容)。
    • FileMode.CreateNew:如果文件已存在,则抛出异常;如果文件不存在,则创建它。
  • 参数三(可选):一个 FileAccess 枚举值,指定文件的访问权限。默认值是 FileAccess.ReadWrite,常见的值有:
    • FileAccess.Read:只允许读取文件。
    • FileAccess.Write:只允许写入文件。
    • FileAccess.ReadWrite:允许读写文件。
  • 参数四(可选):一个 FileShare 枚举值,指定其他进程如何共享该文件。默认值是 FileShare.None,常见的值有:
    • FileShare.Read:允许其他进程读取文件。
    • FileShare.Write:允许其他进程写入文件。
    • FileShare.ReadWrite:允许其他进程读取和写入文件。
    • FileShare.None:不允许其他进程对该文件进行任何共享操作。
FileStream fs = File.Open(Application.dataPath + "/Test2.txt", FileMode.OpenOrCreate);

File.OpenWrite以写模式打开文件

File.OpenWrite 是 File.Open 的一个特定重载,用于以写模式打开文件。如果文件不存在,会创建一个新文件。如果文件已经存在,它会截断文件内容,使文件长度为零,准备写入数据。

  • 打开文件进行写入,文件内容会被截断。
  • 如果文件不存在,会自动创建该文件。
  • 只提供写入访问权限(FileAccess.Write)。
FileStream fs = File.OpenWrite(Application.dataPath + "/Test2.txt");

File.OpenRead以只读模式打开文件

File.OpenRead 也是 File.Open 的一个特定重载,专门用于以只读模式打开文件。如果文件不存在,则会抛出异常。

  • 打开文件进行读取,文件内容不会被修改。
  • 如果文件不存在,会抛出异常。
  • 只提供读取访问权限(FileAccess.Read)。
FileStream fs = File.OpenRead(Application.dataPath + "/Test2.txt");

3、FileStream重要的属性和方法

FileStream fs = File.Open(Application.dataPath + "/Test2.txt", FileMode.OpenOrCreate);

//获取文本字节长度
print(fs.Length);

//是否可写
if(fs.CanRead){
    print("可写");
}

//是否可读
if(fs.CanWrite){
    print("可读");
}

//flush() 方法是用来刷新缓冲区的,即将缓冲区中的数据立刻写入文件,同时清空缓冲区,不需要是被动的等待输出缓冲区写入。一般情况下,文件关闭后会自动刷新缓冲区,但有时你需要在关闭前刷新它,这时就可以使用 flush() 方法。不用Flush相当于一次性写入所有,用了Flush,表示不等后面的,先把当前的写入。
fs.Flush();

//关闭文件流并释放文件资源。可以认为是“完成”操作的标志。
fs.Close();

//释放文件流占用的所有资源,并确保文件流对象可以被垃圾回收。调用 Dispose() 后,文件流不再可用。调用 Dispose() 会隐式调用 Close(),因此您不需要显式地调用 fs.Close()
fs.Dispose();

4、FileStream读取写入

4.1 写入数据

public override void Write(byte[] buffer, int offset, int count);
  • buffer:字节数组,包含要写入的数据。
  • offset:默认值数据开始写入的位置(即数组的起始索引)。
  • count:要写入的字节数。
public override void Write(ReadOnlySpan<byte> buffer);
  • buffer:这是一个 ReadOnlySpan 类型的参数,表示要写入的数据。
写入字节数据
//转换为字节数组
byte[] bytes = BitConverter.GetBytes(666);
FileStream fs = File.Open(Application.dataPath + "/Test1.txt", FileMode.Create, FileAccess.Write);
fs.Write(bytes);
fs.Dispose();
写入字符串数据
FileStream fs2 = File.Open(Application.dataPath + "/Test2.txt", FileMode.Create, FileAccess.Write);
// 字符串数据
string text = "向宇的客栈";
// 转换为字节数据
byte[] bytes2 = Encoding.UTF8.GetBytes(text);
fs2.Write(bytes2);
fs2.Dispose();

4.2 读取数据

public override int Read(byte[] buffer, int offset, int count);
  • buffer:字节数组,存储读取到的数据。
  • offset:存储数据的起始位置(即数组的起始索引)。默认0
  • count:最多要读取的字节数。
  • 返回值:返回实际读取的字节数。
public override int Read(Span<byte> buffer);
  • buffer:一个 Span 类型的数组,用于存储读取的数据。
  • 返回值:返回实际读取的字节数。
读取字节数据
FileStream fs3 = File.Open(Application.dataPath + "/Test1.txt", FileMode.Open, FileAccess.Read);
byte[] bytes3 = new byte[fs3.Length];
fs3.Read(bytes3);
fs3.Dispose();
int i = BitConverter.ToInt32(bytes3, 0);
print("读取字节数据:" + i);

打印结果
在这里插入图片描述

读取字符串数据
FileStream fs4 = File.Open(Application.dataPath + "/Test2.txt", FileMode.Open, FileAccess.Read);
byte[] bytes4 = new byte[fs4.Length];
fs4.Read(bytes4);
fs4.Dispose();
string str = Encoding.UTF8.GetString(bytes4);
print("读取字符串数据:" + str);

打印结果
在这里插入图片描述

4.3 总结:

  • 在 Unity 和现代 .NET 环境中,FileStream 提供了对 Span 和 ReadOnlySpan 类型的支持,允许更高效的文件操作,避免了额外的内存分配。
  • 使用 Write(ReadOnlySpan buffer) 和 Read(Span buffer) 可以在不需要显式创建字节数组的情况下进行文件读写操作,这对于性能优化特别有用,尤其在处理大量数据时。
    在这里插入图片描述
    推荐使用Read(Span<byte> buffer)Write(ReadOnlySpan<byte> buffer)方法

5、using更加安全的使用文件流对象

5.1 using关键字的用法

using(申明一个引用对象)
{
    使用对象
}

无论发生什么情况,当using语句块结束后,会自动调用该对象的销毁方法(也就是使用 using 语句会自动调用 Dispose() 方法),避免忘记销毁或关闭流,using是一种更安全的使用方法。

如果没有使用 using 语句,开发者需要确保在所有可能的退出点(包括异常处理后)调用 fs.Close() 或 fs.Dispose(),这增加了代码复杂性和出错的可能性。

目前我们对文件流进行操作,为了文件操作安全,都用using进行处理最好

5.2 如何证明使用 using 语句会自动调用 Dispose() 方法?

要证明使用 using 语句会自动调用 Dispose() 方法,我们可以重写 FileStream 类,或者创建一个 MemoryStream 类的子类,覆盖其 Flush、Close 和 Dispose 方法,以便打印日志信息,证明哪些方法在 using 语句结束时被调用了。

代码如下

public class TestFileStream : FileStream
{
    public TestFileStream(string path, FileMode mode, FileAccess access) 
        : base(path, mode, access) { }

    public override void Flush()
    {
        base.Flush();
        Console.WriteLine("Flush called");
    }

    public override void Close()
    {
        base.Close();
        Console.WriteLine("Close called");
    }

    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);
        Console.WriteLine("Dispose called");
    }
}
调用 Dispose() 会隐式调用 Close()

包括前面说的调用 Dispose() 会隐式调用 Close(),我们可以先进行验证

TestFileStream fs = new TestFileStream(Application.dataPath + "/Test1.txt", FileMode.Create, FileAccess.Write);
string text = "Hello, Unity World!你好,2024世界!";
byte[] bytes = Encoding.UTF8.GetBytes(text);
fs.Write(bytes, 0, bytes.Length);
fs.Dispose();

结果
在这里插入图片描述

写入
using (TestFileStream fs = new TestFileStream(Application.dataPath + "/Test1.txt", FileMode.Create, FileAccess.Write))
{
    string text = "Hello, Unity World!你好,2024世界!";
    byte[] bytes = Encoding.UTF8.GetBytes(text);
    fs.Write(bytes, 0, bytes.Length);
}

结果
在这里插入图片描述

读取
using (TestFileStream fs = new TestFileStream(Application.dataPath + "/Test1.txt", FileMode.Open, FileAccess.Read))
{
    byte[] bytes = new byte[fs.Length];
    fs.Read(bytes, 0, bytes.Length);
    string str = Encoding.UTF8.GetString(bytes);
    print("读取字符串数据:" + str);
}

结果
在这里插入图片描述

5.3 总结

在C#中,FileStream 是用于读写文件的流类。使用 using 语句来创建和管理 FileStream 对象是一种最佳实践,主要有以下几个原因:

  • 资源管理:FileStream 对象会打开文件并占用系统资源。使用 using 语句可以确保即使在发生异常的情况下,文件也能被正确关闭,释放资源。

  • 异常安全:using 语句创建了一个作用域,在这个作用域结束时,即使发生异常,Dispose 方法也会被调用。Dispose 方法是 IDisposable 接口的一部分,FileStream 实现了这个接口,它确保执行清理工作,比如关闭文件句柄。

  • 代码清晰:使用 using 语句可以使代码更加清晰和易于理解。它清楚地表明了资源的开始和结束使用位置。

  • 避免资源泄漏:如果忘记关闭文件流,可能会导致资源泄漏,比如文件句柄不会被释放,这可能会导致文件在程序结束前无法被访问或修改。

  • 自动垃圾回收:即使 FileStream 对象超出作用域,using 语句确保 Dispose 方法被调用,这有助于垃圾回收器更有效地工作。


六、文件夹操作相关

1、 Directory.Exists判断文件夹是否存在

if (Directory.Exists(Application.dataPath + "/data"))
{
    Debug.Log("文件夹存在!");
}
else
{
    Debug.Log("文件夹不存在!");
}

2、Directory.CreateDirectory创建文件夹

Directory.CreateDirectory(folderPath);

注意
使用 Directory.CreateDirectory 创建文件夹,如果文件夹已经存在,它什么都不做,所以我们并不需要多此一举先判断文件夹是否存在再创建文件夹,直接使用就行

3、Directory.Delete删除文件夹

Directory.Delete(Application.dataPath + "/data", true);
  • 参数一:文件的路径
  • 参数二(可选):是否删除非空目录
    • false:默认值,仅当改目录为空时才删除
    • true:将删除整个目录

注意
指定文件夹不存在报错

4、查找文件夹和文件

4.1 遍历指定路径下所有文件夹名

string[] strs = Directory.GetDirectories(Application.dataPath);
for(int i = 0; i < strs.Length; i++){
print(strs[i]);
}

4.2 遍历指定路径下所有文件名

string[] strs = Directory.GetFiles(Application.dataPath);
for(int i = 0; i < strs.Length; i++){
print(strs[i]);
}

5、移动文件夹

Directory.Move(Application.dataPath + "/data1", Application.dataPath + "/data2");

注意
如果第二个参数所在的路径已经存在了一个文件夹 那么会报错
一定会把文件夹中的所有内容一起移动到新的路径

6、DirectoryInfo获取文件夹的更多信息

DirectoryInfo 类提供了访问和操作目录(文件夹)的方法和属性。可以用它来获取目录的信息、子目录、文件等

6.1 常用属性和方法:

  • FullName:获取目录的完整路径。
  • Name:获取目录的名称。
  • `Parent:获取目录的父目录。
  • Exists:检查目录是否存在。
  • CreationTime:获取目录的创建时间。
  • LastAccessTime:获取目录的最后访问时间。
  • LastWriteTime:获取目录的最后修改时间。
  • GetFiles():获取目录中的所有文件(返回一个 FileInfo 数组)。
  • GetDirectories():获取目录中的所有子目录(返回一个 DirectoryInfo 数组)。

6.2 获取DirectoryInfo信息

创建文件夹返回文件夹信息
DirectoryInfo dInfo = Directory.CreateDirectory(Application.dataPath + "/");
//全路径
print(dInfo.FullName);
//文件名
print(dInfo.Name);
按指定路径获取文件夹信息
string directoryPath = "C:/Users/YourUsername/Documents";
DirectoryInfo dInfo = new DirectoryInfo(directoryPath);

7、FileInfo获取文件的更多信息

FileInfo 类用于获取关于文件的详细信息,如文件的大小、创建时间、修改时间等。

7.1 常用属性和方法:

  • FullName:获取文件的完整路径。
  • Name:获取文件的名称(包括扩展名)。
  • Length:获取文件的大小(以字节为单位)。
  • CreationTime:获取文件的创建时间。
  • LastAccessTime:获取文件的最后访问时间。
  • LastWriteTime:获取文件的最后修改时间。
  • Exists:检查文件是否存在。
  • Delete():删除文件。

7.2 获取FileInfo信息

按DirectoryInfo信息获取目录中的所有文件信息

返回的 FileInfo[] 数组包含了该目录下所有文件的信息,但不包含子目录。

DirectoryInfo dInfo = Directory.CreateDirectory(Application.dataPath + "/");
FileInfo[] fileInfo = dInfo.GetFiles();
for(int i = 0; i < fileInfo.Length; i++){
print(fInfos[i].Name);//文件名
print(fInfos[i].FullName);//路径
print(fInfos[i].Length);//字节长度
print(fInfos[i].Extension);//后缀名
}
按指定文件路径获取文件信息
string filePath = "C:/Users/YourUsername/Documents/example.txt";
FileInfo fileInfo = new FileInfo(filePath);

六、BinaryFormatter将C#类对象进行二进制序列化和反序列化

1、引入必要的命名空间

为了进行二进制序列化和反序列化,你需要使用以下命名空间:

using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

2、序列化

2.1 申明类对象

注意:如果要使用C#自带的序列化二进制方法,申明类时需要添加[System.Serializable]特性。如果没有这个特性,BinaryFormatter 将无法处理该类。

[System.Serializable]
public class Man
{
    public string name = "向宇的客栈";
    public int age = 18;
    public float height = 1.75f;
    public int[] ints = new int[] { 1, 2, 3, 4 };
    public List<int> list = new List<int>() { 1, 2, 3, 4, 5 };
    public Dictionary<int, string> dic = new Dictionary<int, string>() { { 1, "字典数据1" }, { 2, "字典数据2" } };
    public ClassTets ct = new ClassTets();
    public StructTest st = new StructTest();
}

[System.Serializable]
public struct StructTest{
    public string name;
}

[System.Serializable]
public class ClassTets{
    public string name = "class类";
}

2.2 将对象进行二进制序列化

方法一:使用内存流得到二进制字节数组

主要用于得到字节数组可以用于网络传输

void Start()
{
    Man man = new Man();
    
    using (MemoryStream ms = new MemoryStream())
    {
        //二进制格式化程序
        BinaryFormatter bf = new BinaryFormatter();
        
        //序列化对象 生成二进制字节数组 写入到内存流当中
        bf.Serialize(ms, man);
        
        //得到对象的二进制字节数组
        byte[] bytes = ms.GetBuffer();
        
        //存储字节
        File.WriteAllBytes(Application.dataPath + "/Test.xy", bytes);
    }
}

效果
在这里插入图片描述

方法二、使用文件流进行存储

主要用于存储到文件中

void Start()
{
    Man man = new Man();
   
    using (FileStream fs = File.OpenWrite(Application.dataPath + "/Test.xy")){
      //申明一个二进制格式化类
        BinaryFormatter bf = new BinaryFormatter();
        
        //序列化对象 生成2进制字节数组 写入到内存流当中
        bf.Serialize(fs, man);
     }
}

结果
在这里插入图片描述

3、反序列化

3.1 反序列化网络传输过来的二进制数据

//目前没有网络传输 我们还是直接从文件中获取
byte[] bytes = File.ReadAllBytes(Application.dataPath + "/Test.xy");
//申明内存流对象 一开始就把字节数组传输进去
using (MemoryStream ms = new MemoryStream(bytes))
{
    //申明一个 2进制格式化程序
    BinaryFormatter bf = new BinaryFormatter();
    //反序列化
    Man res = bf.Deserialize(ms) as Man;
}

结果
在这里插入图片描述

3.2 反序列化文件中的数据

//通过文件夹流打开指定的2进制数据文件
using (FileStream fs = File.OpenRead(Application.dataPath + "/Test.xy")){
    //申明一个二进制格式化类
    BinaryFormatter bf = new BinaryFormatter();
    //反序列化
    Man res = bf.Deserialize(fs) as Man;
}

在这里插入图片描述

4、总结

  • 使用 BinaryFormatter 可以方便地将 C# 对象序列化为二进制数据并保存到文件中,也可以将二进制数据反序列化回 C# 对象。
  • 需要确保类标记为 [Serializable]。如果没有这个特性,BinaryFormatter 将无法处理该类。
  • Unity 中的类型(如 Vector3、Color、Quaternion 等)不能直接通过 BinaryFormatter 序列化,需要进行手动转换或考虑使用其他序列化方式。
  • 可以直接序列化private、protected私有字段
  • 反序列化时要小心安全性问题,尽量避免反序列化不受信任的数据。

七、封装二进制数据管理器

学习了前面的知识,相信你已经知道了如何进行二进制数据的保存与读取

现在我们封装一个二进制数据管理器,方便我们实际项目对二进制数据的保存与读取

1、二进制数据管理器代码

using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using UnityEngine;

/// <summary>
/// 用于管理二进制数据的保存与读取
/// </summary>
public class BinaryDataMgr
{
    // 单例模式,保证类只有一个实例
    private static BinaryDataMgr instance = new BinaryDataMgr();
    public static BinaryDataMgr Instance = instance; // 提供外部访问实例的静态属性

    // 私有构造函数,防止外部实例化
    private BinaryDataMgr() { }

    // 数据存储的位置,使用应用程序的持久化数据路径
    private static string SAVE_PATH = Application.persistentDataPath + "/SaveData/";

    /// <summary>
    /// 存储类对象数据
    /// </summary>
    /// <param name="obj">要保存的对象</param>
    /// <param name="fileName">文件名</param>
    public void Save(object obj, string fileName)
    {
        // 检查存储路径是否存在,如果不存在则创建该文件夹
        if (!Directory.Exists(SAVE_PATH))
            Directory.CreateDirectory(SAVE_PATH);

        // 使用文件流写入数据
        using (FileStream fs = File.OpenWrite(SAVE_PATH + fileName + ".txt"))
        {
            // 创建二进制格式化器
            BinaryFormatter bf = new BinaryFormatter();
            // 将对象序列化为二进制数据并写入文件
            bf.Serialize(fs, obj);
        }
    }

    /// <summary>
    /// 读取二进制数据并将其转换成对象
    /// </summary>
    /// <typeparam name="T">返回对象的类型</typeparam>
    /// <param name="fileName">文件名</param>
    /// <returns>反序列化后的对象</returns>
    public T Load<T>(string fileName) where T : class
    {
        // 如果文件不存在,直接返回该类型的默认值
        if (!File.Exists(SAVE_PATH + fileName + ".txt"))
            return default(T);

        T obj;
        // 使用文件流读取数据
        using (FileStream fs = File.OpenRead(SAVE_PATH + fileName + ".txt"))
        {
            // 创建二进制格式化器
            BinaryFormatter bf = new BinaryFormatter();
            // 将文件内容反序列化为对象
            obj = bf.Deserialize(fs) as T;
        }
        
        return obj;
    }
}

2、测试调用

[System.Serializable]
public class Man
{
    public string name = "向宇的客栈";
    public int age = 18;
    public float height = 1.75f;
    public int[] ints = new int[] { 1, 2, 3, 4 };
    public List<int> list = new List<int>() { 1, 2, 3, 4, 5 };
    public Dictionary<int, string> dic = new Dictionary<int, string>() { { 1, "字典数据1" }, { 2, "字典数据2" } };
    public Friend ct = new Friend("向宇");
    public List<Friend> friends = new List<Friend>() { new Friend("小明"), new Friend("小红")};
    private int privateInt = 1;
    protected int protectedInt = 2;
}

[System.Serializable]
public class Friend
{
    public string name;

    public Friend(string name){
        this.name = name;
    }
}


public class Test : MonoBehaviour
{
    void Start()
    {
        Man man = new Man();

        string fileName = "文件名";
        // 使用 BinaryDataMgr 实例将对象保存到文件中
        BinaryDataMgr.Instance.Save(man, fileName);

        // 使用 BinaryDataMgr 实例从文件中加载对象
        Man res = BinaryDataMgr.Instance.Load<Man>(fileName);

        // 输出加载后的数据
        print(res.name);
        print(res.age);
    }
}

3、测试结果

在这里插入图片描述

专栏推荐

地址
【从零开始入门unity游戏开发之——C#篇】
【从零开始入门unity游戏开发之——unity篇】
【制作100个Unity游戏】
【推荐100个unity插件】
【实现100个unity特效】
【unity框架开发】

完结

赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注,你的每一次支持都是我不断创作的最大动力。当然如果你发现了文章中存在错误或者有更好的解决方法,也欢迎评论私信告诉我哦!

好了,我是向宇https://xiangyu.blog.csdn.net

一位在小公司默默奋斗的开发者,闲暇之余,边学习边记录分享,站在巨人的肩膀上,通过学习前辈们的经验总是会给我很多帮助和启发!如果你遇到任何问题,也欢迎你评论私信或者加群找我, 虽然有些问题我也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~
在这里插入图片描述


原文地址:https://blog.csdn.net/qq_36303853/article/details/144169412

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!