抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

在硬盘里保存txt或二进制文件非常容易,当需要保存的对象是一个自定义类的对象时,此时采用txt或二进制存储都较为复杂,如果采用txt形式,那么在保存非文本的数据时,需要手动转换,并且txt非常容易修改。保存为二进制文件较为简单,C#还提供了int32,byte等类型的读写方法,可以直接使用,但是仍有弊端,即代码复杂,你需要不断地读取,赋值。

实际上C#提供了序列化存储的方法,可以轻松地把一个对象保存到硬盘里。

序列化储存方法

首先定义一个类,并在最前面加上”[Serializable]”,表示这个类可以序列化

[Serializable]
class Struct
{
    public string a { get; set; }
    public string b { get; set; }
    public int s { get; set; }
 
}

引入命名空间System.Runtime.Serialization.Formatters.Binary和System.IO

实例化对象@struct,并保存在D:\abc.txt

static void Save()
{
    Struct @struct = new Struct();
    @struct.a = "123";
    @struct.b = "ABCD";
    @struct.s = 27;
    using (FileStream fileStream = new FileStream(@"D:\abc.txt", FileMode.OpenOrCreate))
    {
        BinaryFormatter binaryFormatter = new BinaryFormatter();
        binaryFormatter.Serialize(fileStream, @struct);
    }
}

从硬盘读取

static void Read()
{
    using(FileStream fileStream = new FileStream(@"D:\abc.txt", FileMode.OpenOrCreate))
    {
        BinaryFormatter binaryFormatter = new BinaryFormatter();
        Struct @struct = binaryFormatter.Deserialize(fileStream) as Struct;
        Console.WriteLine(@struct.a);
        Console.WriteLine(@struct.b);
        Console.WriteLine(@struct.s);
    }
}

运行结果
DearXuan

原理研究

增加一个变量

首先给这个类加上一个变量k

[Serializable]
class Struct
{
    public string a { get; set; }
    public string b { get; set; }
    public int s { get; set; }
 
    public int k { get; set; }
 
}

执行Read方法,发现竟然读取成功了,但是k并没有被赋值

增加一个方法

我们再给他加上一个方法ABC

class Struct
{
    public string a { get; set; }
    public string b { get; set; }
    public int s { get; set; }
 
    public int k { get; set; }
 
    public void ABC()
    {
        k++;
    }
 
}

现在这个类的结果已经完全改变,但是执行了Read方法后发现仍然读取成功了。

修改一个变量

现在我们修改其中一个变量名,把s改成ss

[Serializable]
class Struct
{
    public string a { get; set; }
    public string b { get; set; }
    public int ss { get; set; }
 
}

执行结果

DearXuan

修改类名

最后我们把类名Struct改成Struct1\

[Serializable]
class Struct1
{
    public string a { get; set; }
    public string b { get; set; }
    public int s { get; set; }
 
}

不出所料地失败了

DearXuan

分析结论

到这里已经基本可以看出序列化储存地原理,C#采用了类似XML文件地方法,将类名,变量名与变量值保存到一起。

将保存的文件以二进制形式打开

DearXuan

搜索字符串”123”

DearXuan

发现就在这个字符串的后面出现了连续的41到45,很明显这就是”ABCDE”的ASCII码,只不过是16进制的,换成10进制就看着舒服多了

DearXuan

同时我们还注意到这些字符串的前面都有一个数字恰好是后面的字符串长度

例如: “03”代表后面3个字节是字符串,但是为什么”03”前面还有3个”00”?在VS里随意输入一个字符串.Length,查看Length的类型

DearXuan

Length返回的是int类型,这说明string的最大长度不会超过int的最大值,int是int32的别名,从名字就能看出int32占了32位,恰好是4个字节,这也证明string的储存形式是 “长度+内容”

继续往后看,最后面还有一个int类型的数字27,但是这个27的位置很奇怪,他居然是靠左的,而刚刚还是靠右的。为了进一步研究,我们把27改成999999999

DearXuan

现在的十六进制码是 FF C9 9A 3B。通过其他软件进制转换,发现正确的十六进制码应该是

3B 9A C9 FF。恰好是上面的反转。

我们再把int改成long,并把数字改成99999999999999999,再次尝试。

DearXuan

软件中是:FF FF 89 5D 78 45 63 01

实际上是:01 63 45 78 5D 89 FF FF

已经足够肯定C#会将数字倒序输出。但是这样不是多此一举吗?大家是否还记得在进制转换时需要不断计算余数,最后把余数倒序排列?并且这个规律只在16进制出现,合理猜测C#在保存数值类型数据时会把数字转化成16进制来保存,并且没有倒序输出。而十六进制转十进制时,也是需要从右往左来读取,第一个数的权值是1,第二个数是16,第三个是16^2。不管是保存还是读取,都是需要从右往左的,因为右边是最低位。

生活中进制转换需要把余数倒序排列,因为我们的数字是高位在左,低位在右,而计算机储存时显然不需要遵守这个规律,它可以令高位在右,低位在左,这样就省去了倒序输出这一步,并且也符合了文件流操作从左到右的顺序。

至于为什么选择16进制,而不是二进制,可能是为了效率,同样的一个数2^16,如果除以16,则只需要计算4次,但是如果除以2,则需要计算16次,效率相差了4倍。但是最终不是还要用二进制保存吗?是不是还需要把16进制转成2进制?如果是我们自己写代码,可能真的需要多转换一次,但是计算机是以2进制保存数据,除法的底层原理是位移计算,计算结果也是2进制数,所以计算机并不需要额外转换一次。

想到这里,看似已经真相大白,但是又出现了新的问题,我们输入的数字在内存里也是二进制形式,计算机可以直接把这个二进制形式的数字从左到右保存到文件里,为什么要多此一举先转换成10进制,再转成16进制?

接下来我们用C++进行下一步实验

DearXuan

图中可以看到,我在计算机中保存了0x11223344这个数字,尽管我输入的是16进制,但是内存里仍然是按int类型储存。C++中的char只占了一个字节,所以我们定义一个char指针,把他指向a,此时p指向的是a的第一个字节,顺序打印p,p+1,p+2,p+3位置的数据,发现结果是 44 33 22 11。

这样的结果与我们的输入完全相反,但同时也证明了int在内存中是倒序存放的(相对于人类是倒序)。与上面的猜想相联系,最终得出答案:数字在内存中是以字节为单位倒序保存的,这样保存的优点在于可以从低位到高位的读取方向与流操作从左到右的方向相同,加快了读取速度。

最后回到一开始的问题上来,我们已经研究了变量在序列化操作中的保存方法。如果刚刚仔细观察,会发现文件末尾总是 0B,由此我们可以大胆猜测这是结束符。我们还发现字符串的前面除了有四个字节用来表示数字以外,还有两个字节 06 03,以及第二个字符串前面的 06 04,如果你在类里面多定义一个字符串,你会发现字节码里多出一个 06 05,因此我们也可以大胆猜测这是字符串标志。为了进一步验证这个猜想,我们把前面的0603改成0604,把后面的0604改成0603,重新读取,发现能够正常读取,而如果把前面的06改成07,就无法读取了,可以证明06是标识符。后面的数据是按顺序存储的,而不是采用指针的方法。

到这里已经基本搞清楚变量的储存结构了,前面一长串的字节可以直接用txt格式打开,能够发现其中夹杂着Version,PublicKeyToken,Struct等,这些数据表明了版本,类的结构,类里面的变量名等数据,用来判断类的格式。后面紧跟着的是变量,变量与前面的变量名按顺序一一对应,最后一位是0B,表示文件流结束。

评论