.NET陷阱之六:从枚举值持久化带来大量空间消耗谈起

好长时间没有写博文了,今天继续。

这次跟大家分享的内容起因于对一个枚举值列表的序列化,下面简化后的代码即能重现。为了明确起见,我显式指定了枚举的基础类型。

// 定义一个枚举类型。
public enum SomeEnum :int
{
    First,
    Second,
    Third,
    ... ...
}

// 重现问题的代码。
var list = new List<SomeEnum>();
for (int i = 0; i < 1000; ++i)
{
    list.Add((SomeEnum)(i % 3));
}

var formatter = new BinaryFormatter();
var stream = File.OpenWrite("c:\\a.data");
formatter.Serialize(stream, list);
stream.Close()

你预料生成的a.data文件大约有多大?

  • 如果你估计的结果是12K以上,那么你应该知道我要说什么了,可以洗洗睡了;
  • 如果你估计的结果是4K多一些,那么请继续看本文后面的内容。

得到4K结果的同学,我想是这样估计的,SomeEnum枚举用int表示,每个值占用4字节,1000个大约就是4K左右,加上其它一些序列化信息,可能就4K多一些吧。最初我也是这么想的,直到在软件中这样的列表占用了几十兆的内存时,问题才暴露出来。我想我还是比较天真,以为那么简洁的类型应该有相应简洁的序列化方式,我甚至天真到从来没有意识到这是个问题。

我用Reflector跟踪了具体的持久化过程,才发现原来在.NET framework内部,对枚举值并没有像基本类型那样进行处理,而是直接当成普通的值对象处理的。更糟糕的是,对于值对象的处理,居然也要像引用对象那样保存objectId和mapId。我用了“居然”这个词,因为我真的认为值对象(ValueType)就只是数据,不会存在两个reference引用同一个值对象的情况(我知道这样说有些奇怪,但希望你能明白我的意思)——直到现在我也这么认为。

下面是 formatter.Serialize(stream, list) 这句代码执行过程中某一时刻的堆栈状态,为了避免大量的折行影响你的心情,我只保留了函数名部分。

 mscorlib.dll!System.Runtime.Serialization.Formatters.Binary.BinaryObject.Write(...)
 mscorlib.dll!System.Runtime.Serialization.Formatters.Binary.__BinaryWriter.WriteObject(...)
 mscorlib.dll!System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Write(...)
 mscorlib.dll!System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Write(...)
 mscorlib.dll!System.Runtime.Serialization.Formatters.Binary.ObjectWriter.WriteArrayMember(...)
 mscorlib.dll!System.Runtime.Serialization.Formatters.Binary.ObjectWriter.WriteArray(...)
 mscorlib.dll!System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Write(...)
 mscorlib.dll!System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Serialize(...)
 mscorlib.dll!System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize(...)
 mscorlib.dll!System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize(System.IO.Stream serializationStream, object graph) 

在栈顶上是.NET framework二进制序列化中BinaryObject.Write方法,其实现如下:

public void Write(__BinaryWriter sout)
{
    sout.WriteByte(1);
    sout.WriteInt32(this.objectId);
    sout.WriteInt32(this.mapId);
}

也就是说每写一个枚举值,系统都会先写入1 + 4 + 4 = 9个字节的额外数据!这样算起来,开始处代码产生的文件就大约是 1K * (9 + 4) = 13K !

这几天我一直在想:为什么对值对象也要写入objectId和mapId呢?根据框架的代码的实际输出来看,系统不会“对值相等的多个值对象只保存一份数据”,那么为什么还要写入这些额外的数据呢?对此我仍不得其解,如果有人知道,还请不吝赐教。

为了解决这个问题,我在类型内部使用了List<int>来保存数据,而在对外接口中完成int和SomeEnum的转换,这样做一来不会影响其它模块的代码,二来也可以将此处理进行屏蔽。

基于同样的原因,对于如下一个值类型来说,要直接使用.NET提供的序列化机制,则每保存一个对象,将额外消耗一倍多的空间。是的,对于引用类型来说也是一样,但还是那句话——我只是没有意识到这个问题,或者说现在还不能接受framework那么粗糙的实现!

[Serializable]
public struct Point
{
    private float x, y;
}

为了避免这样的问题,最直接的方法是在包含此类成员的类型上实现ISerializable接口,然后存储转换到基本类型的数据。如果类中要序列化的成员比较多的话,这样做可能会导致其它成员也要手工处理。如果感兴趣,也可以参考我的另一篇博文《深入挖掘.NET序列化机制——实现更易用的序列化方案》看看能不能实现一个统一的机制。

最后再次呼吁:有谁能告诉我微软为什么要如此处理值类型的序列化?

.NET陷阱之六:从枚举值持久化带来大量空间消耗谈起,布布扣,bubuko.com

时间: 2024-08-27 00:56:22

.NET陷阱之六:从枚举值持久化带来大量空间消耗谈起的相关文章

枚举位预算 (适用于权限和拥有多种枚举值)

一.基础知识 什么是位运算? 用二进制来计算,1&2:这就是位运算,其实它是将0001与0010做位预算   得到的结果是 0011,也就是3 2.位预算有多少种?(我们就将几种我们权限中会用到的) &  与运算    1&0=0    1&1=1   0&0=0 |   或运算    1|1=1     1|0=1    0|0=0 ~  非运算    ~1=0      ~0=1 二.如何与权限关联         1.逻辑是什么?         其实逻辑很简

让枚举值与字符串一一对应

说明 统一管理字符串,可以用数值来代表字符串,其目的就是为了增加代码的可读性. 源码 https://github.com/YouXianMing/StringAndValue // // StringAndValue.h // StringAndValue // // Created by YouXianMing on 15/6/9. // Copyright (c) 2015年 YouXianMing. All rights reserved. // #import <Foundation/

在C#中如何读取枚举值的描述属性

在C#中,有时候我们需要读取枚举值的描述属性,也就是说这个枚举值代表了什么意思.比如本文中枚举值 Chinese ,我们希望知道它代表意思的说明(即“中文”). 有下面的枚举: 1 2 3 4 5 6 public enum EnumLanugage {     [System.ComponentModel.Description("中文")]     Chinese,     English } 我们要获取的就是 Chinese 中的说明文字“中文”. 1 2 3 4 5 6 7 8

返回枚举值的描述 根据枚举类型返回类型中的所有值、文本及描述

/// <summary> /// 返回枚举值的描述 /// </summary> /// <param name="value">枚举值</param> /// <returns>指定枚举值描述</returns> public static string getEnumDescription(this Enum value) { FieldInfo fi = value.GetType().GetField(v

利用DescriptionAttribute定义枚举值的描述信息 z

System.ComponentModel命名空间下有个名为DescriptionAttribute的类用于指定属性或事件的说明,我所调用的枚举值描述信息就是DescriptionAttribute类的Description属性值. 首先定义一个枚举 /// <summary>    /// 测试用的枚举    /// </summary>    public enum ArticleTypeList    {        [DescriptionAttribute("

IOS开发基础篇 --添加的约束中所有的枚举值

/** NSLayoutConstraint类中的枚举值 *  代码添加一条约束      *      * @param Item:view1 :要约束的控件      * @param attribute:attr1 :约束的类型(做怎样的约束)      * @param relatedBy:relation :与参照控件之间的关系      * @param toItem:view2 :参照的控件      * @param attribute:attr2 :约束的类型(做怎样的约束)

C#枚举扩展方法,获取枚举值的描述值以及获取一个枚举类下面所有的元素

/// <summary> /// 枚举扩展方法 /// </summary> public static class EnumExtension { private static Dictionary<string, Dictionary<string, string>> _enumCache; /// <summary> /// 缓存 /// </summary> private static Dictionary<stri

Kendo Grid控件中将枚举值转为枚举名显示

我们在开发过程中经常会遇到需要将枚举值转换成名称进行显示的情况.如下我们有这样一个数据源对象: var users = [ {id: 1, name: "similar1", status: 1}, {id: 2, name: "similar2", status: 2} ]; 其中字段 status 代表的是用户的状态, 1 代表 “可用”, 2 代表 “禁用”.我们使用 kendo grid 常规配置如下: columns: [ { field: "i

C#枚举总结和其扩展用法(通过枚举描设置枚举值)

C#中枚举是一个非常好用的类型,用会了之后确实方便了很多. 项目中一个枚举类型: public enum Version_Type : byte { [Description("1997版")] 版本1997 = 0 , [Description("2007版")] 版本2007 } 枚举类型的默认类型是int型,可以改变其使用的类型,需要用(: <type>)来进行设置,上例中<type>为byte,也可以用其它类型(byte,sbyte,