如何在C#中模拟C++的联合(Union)?[C#, C++] How To Simulate C++ Union In C#?

1 什么是联合?

联合(Union)是一种特殊的类,一个联合中的数据成员在内存中的存储是互相重叠的。每个数据成员都在相同的内存地址开始。分配给联合的存储区数量是“要包含它最大的数据成员”所需的内存数。同一时刻只有一个成员可以被赋给一个值。

下面我们来看看C++中如何表达联合:

// Code #01
union TokenValue
{
    char _cval;
    int _ival;
    double _dval;
};

2 联合的内存布局与内存使用情况。

下面我们来考察一下TokenValue的内存布局。

首先,我们使用sizeof运算符来获取该联合各个成员的内存占用字节数:

// Code #02
int _tmain(int argc, _TCHAR* argv[])
{
    cout << "sizeof(char): " << sizeof(char) << endl;
    cout << "sizeof(int): " << sizeof(int) << endl;
    cout << "sizeof(double): " << sizeof(double) << endl;

    return 0;
}

/*
 * Output:
 * sizeof(char): 1
 * sizeof(int): 4
 * sizeof(double): 8
 *
 */

这样,分配给该联合的内存就是8个字节。

接着,我们来看看具体使用该联合时,所分配的内存的字节占用情况如何:

// Code #03
int _tmain(int argc, _TCHAR* argv[])
{
    TokenValue tv;
    // [_][_][_][_][_][_][_][_]

    tv._cval = ‘K‘;

    // [X][_][_][_][_][_][_][_]

    tv._ival = 1412;

    // [X][X][X][X][_][_][_][_]

    tv._dval = 3.14159;

    // [X][X][X][X][X][X][X][X]

    return 0;
}

3 第一次尝试:在C#中模拟这种布局方式。

在C#中,要指定成员的内存布局情况,我们需要结合使用StructLayoutAttribute特性、LayoutKind枚举和FieldOffsetAttribute特性,它们都位于System.Runtime.InteropServices命名空间中。

下面我用struct来试着模拟上面的TokenValue联合:

// Code #04
[StructLayout(LayoutKind.Explicit, Size=8)]
struct TokenValue
{
    [FieldOffset(0)]
    public char _cval;

    [FieldOffset(0)]
    public int _ival;

    [FieldOffset(0)]
    public double _dval;
}

我们知道,联合的每个数据成员都在相同的内存地址开始,通过把[FieldOffset(0)]应用到TokenValue的每一个成员,我们就指
定了这些成员都处于同一起始位置。当然,我们得事先告诉.NET这些成员的内存布局由我们来作主,把LayoutKind.Explicit枚举传递给
StructLayoutAttribute特性的构造函数,并应用到TokenValue,.NET就不会再干涉该struct的成员在内存中的布局
了。另外,我显式的把TokenValue的大小设置为8字节,当然,这样做是可选的。

4 在实际的C++代码中,我们是如何使用联合的?

在实际的C++代码中,我们应尽量避免让客户端直接使用联合,Code
#03就是一个很好的反面例子了。为什么呢?熟悉C/C++的开发人员都知道,联合提供我们这样一个节省空间的储存方式,是要我们付出一定的代价的。这个
代价就是代码的安全性,不恰当地使用联合可能会导致程序崩溃的。

由于每一次只有一个联合成员处于激活状态,如果我们不小心或者因为其它原因使用处于休眠状态的成员,轻则得到错误的结果,重则整个程序中止。请看下面的代码:

// Code #05
union TokenValue
{
    char _cval;
    int _ival;
    double _dval;
    char* _sval;
};

int _tmain(int argc, _TCHAR* argv[])
{
    TokenValue tv;
    tv._cval = ‘K‘;
    cout << tv._cval << endl;    // Line #01
    cout << tv._ival << endl;    // Line #02
    cout << tv._dval << endl;    // Line #03
    cout << tv._sval << endl;    // Line #04

    return 0;
}

这里的TokenValue比起Code #01的仅仅多了一个_sval,它是C风格的字符串,实质上,它是指向字符串的第一个字符的指针,它占用4字节的内存空间。

当程序运行到Line
#04时,就会出现Unhandled
Exception,程序中止,并指出_sval的值非法(即所谓的“野指针”)。程序无法把它的值输出控制台,然而,Line #01 ~ Line
#03都能输出,只是Line #02和Line #03所输出的值是错误的而已。

实际的应用中,我们一般不会看到如此低级且显而易见的错误,但复杂的实际应用中,不恰当地使用联合的确会为我们带来不少的麻烦。

5 第二次尝试:改进型的联合模拟。

一般情况下,联合作为一种内部数据的储存手段,没有必要让客户端对其有所了解,更没必要让客户端直接使用它。为了使我们的联合模拟用起来更安全,我们需要对它进行一番包装:

// Code #06
class Program
{
    static void Main(string[] args)
    {
        Token t = new Token();

        Console.WriteLine(t);
        Console.WriteLine(t.GetTokenValue());

        t.SetTokenValue(‘K‘);
        Console.WriteLine(t);
        Console.WriteLine(t.GetTokenValue());
    }
}

public struct Token
{
    private TokenValue tv;
    private TokenKind tk;

    public void SetTokenValue(char c)
    {
        tk = TokenKind.CharValue;
        tv._cval = c;
    }

    public void SetTokenValue(int i)
    {
        tk = TokenKind.IntValue;
        tv._ival = i;
    }

    public void SetTokenValue(double d)
    {
        tk = TokenKind.DoubleValue;
        tv._dval = d;
    }

    public object GetTokenValue()
    {
        switch (tk)
        {
            case TokenKind.CharValue:
                return tv._cval;
            case TokenKind.IntValue:
                return tv._ival;
            case TokenKind.DoubleValue:
                return tv._dval;
            default:
                return "NoValue";
        }
    }

    public override string ToString()
    {
        switch (tk)
        {
            case TokenKind.CharValue:
                return tv._cval.ToString();
            case TokenKind.IntValue:
                return tv._ival.ToString();
            case TokenKind.DoubleValue:
                return tv._dval.ToString();
            default:
                return "NoValue";
        }
    }

    [StructLayout(LayoutKind.Explicit, Size = 8)]
    private struct TokenValue
    {
        [FieldOffset(0)]public char _cval;
        [FieldOffset(0)]public int _ival;
        [FieldOffset(0)]public double _dval;
    }

    private enum TokenKind
    {
        NoValue,
        CharValue,
        IntValue,
        DoubleValue
    }
}

/* 
 * Output:
 * NoValue
 * NoValue
 * K
 * K
 *
 */

由于Token是值类型,实例化时,对应的成员(tv和tk)会自动被赋予与之对应的零值。此时,tv._cval为‘\0‘、tv._ival和tv._dval均为0(实质上它们是同一个值在不同的类型中的表现)。而tk也被自动赋予0:

tk = 0;

这里,你无需进行强类型转换,0是任何枚举的默认初始值,.NET会负责把0转换成对应的枚举类型。例如,你可以:

// Code #07
System.DayOfWeek d = 0;
Console.WriteLine(d);

该代码能正确输出Sunday——一个星期的第一天(西方习惯),也是该枚举的第一个成员。

一般情况下,0对应着枚举的第一个成员(除非你在定义枚举的时候,把第一个成员指定为别的值,并为别的成员赋予0值)。这样,我们就不难看出代码的输出是合理的,而且代码本身也是安全的。

6 别在模拟的联合中同时使用值类型和引用类型!

到目前为止,我们所模拟的联合中,所有的成员都是值类型,如果我们为它加入一个引用类型,例如String呢?

// Code #08
[StructLayout(LayoutKind.Explicit, Size=8)]
struct TokenValue
{
    [FieldOffset(0)]
    public char _cval;

    [FieldOffset(0)]
    public int _ival;

    [FieldOffset(0)]
    public double _dval;

    [FieldOffset(0)]
    public string _sval;
}

这样,Code #06的代码运行时就会提示出错:

Could
not load type ‘TokenValue‘ from assembly ‘UnionLab,
Version=1.0.1820.28531, Culture=neutral, PublicKeyToken=null‘ because it
contains an object field at offset 0 that is incorrectly aligned or
overlapped by a non-object field.

TokenValue初始化的时候,_cval、_ival和_dval都能正确的被赋予对应的零值,而这些零值也能被统一起来(别的值就不行
了)。但_sval不同,它是引用类型,如果没有显示初始化为某个有意义的值,它将被赋予null值!这个null值跟之前的有意义的零值是不能被统一起
来的!所以,要么你就去掉这个_sval,要么就重新定义它的起始位置(当然,你也得去掉Size=8!),但这样一来,TokenValue就不再称得
上联合的模拟了。

在C++中,我们可以直接使用指针来解决这个问题,如Code #05,但C#中,问题就会变得有点辣手。如果你有兴趣的话,可以使用不安全代码(Unsafe code)来试着解决,但这样一来,你的代码又会引入一些新的问题。

7 为什么要在C#里面模拟这个用处不大的东西?[NEW]

相信很多人都有这样一个疑问:为什么要在C#里面模拟这个
用处不大的东西?就我个人来说,我始终坚信事物的存在必定有它的理由,否则就不会存在。其实,联合在我们平时的编码中的确很少用到,但在某些情况下,我们
必须使用它!.NET为我们提供巨大的便利的同时,也不忘让我们能够与非托管代码交互。你知道,早期的Win32
API使用C来完成的,这里面就有很多函数的参数是以联合的形式表达的,要在C#中跟这些API交互,我们就得“尊重”原函数的用法约束。

8 终点与起点的交界处。

回顾整个探索旅程,我们为了使用联合节省空间的优势,开始了这个模拟的探索,然而,为了弥补联合的不足,我们对这个模拟进行了一番包装,增加了不少
额外的代码,直到后来,又发现了在这个模拟中同时使用值类型的成员和引用类型的成员所引发的问题,我们一直都没有停止过探索和思考。正如马斯洛的需要层次
理论所描述的,人只要低层次的需要被满足,马上就会转向更高的需要层次,一级一级的,直到攀上最高峰为止。

关于在C#中模拟C++的联合这个话题,我并没有在本文中给予你一个完整的展示,相反,我为你展示的仅仅是一个探索的起点,希望为你带来一丝灵感,让你根据自己的实际情况来定制你的探索旅程。Have a good trip!



参考资料:

时间: 2024-10-03 23:11:02

如何在C#中模拟C++的联合(Union)?[C#, C++] How To Simulate C++ Union In C#?的相关文章

如何在postgresql中模拟oracle的dual表,来测试数据库最基本的连接功能?

还好,网上弄到的,,没有dual的数据库,可以试图用select函数不带from数据表的方式来实现返回值. 一段测试代码: try: conn = psycopg2.connect(database=db.service_name, user=db.username, password=password, host=db.ip, port=db.port) cursor = conn.cursor() except Exception, e: context_dict = {'msg': e,

[转载]如何在C++03中模拟C++11的右值引用std::move特性

本文摘自: http://adamcavendish.is-programmer.com/posts/38190.htm 引言 众所周知,C++11 的新特性中有一个非常重要的特性,那就是 rvalue reference,右值引用. 引入它的一个非常重要的原因是因为在 C++ 中,常常右值,通俗地讲"在等号右边的"临时变量或者临时对象,我们是无法得到它的修改权限的. 由于类的构造和析构机制,往往产生的临时变量或临时对象的拷贝构造及析构,会带来不少的时间.资源消耗. 也同样由于这样的限

如何在java中使用sikuli进行自动化测试

很早之前写过一篇介绍sikuli的文章.本文简单介绍如何在java中使用sikuli进自动化测试. 图形脚本语言sikuli sikuli IDE可以完成常见的单击.右击.移动到.拖动等鼠标操作,java引用sikuli-script.jar同样可以执行这些常见的鼠标操作,因此即可方便的编写java实现识别图片并模拟点击/拖动目标控件. sikuli-script.jar:http://download.csdn.net/download/hqd1986/4557974 将sikuli-scri

如何在SQLServer中处理每天四亿三千万记录

首先声明,我只是个程序员,不是专业的DBA,以下这篇文章是从一个问题的解决过程去写的,而不是一开始就给大家一个正确的结果,如果文中有不对的地方,请各位数据库大牛给予指正,以便我能够更好的处理此次业务. 项目背景 这是给某数据中心做的一个项目,项目难度之大令人发指,这个项目真正的让我感觉到了,商场如战场,而我只是其中的一个小兵,太多的战术,太多的高层之间的较量,太多的内幕了.具体这个项目的情况,我有空再写相关的博文出来. 这个项目是要求做环境监控,我们暂且把受监控的设备称为采集设备,采集设备的属性

我是如何在SQLServer中处理每天四亿三千万记录的

首先声明,我只是个程序员,不是专业的DBA,以下这篇文章是从一个问题的解决过程去写的,而不是一开始就给大家一个正确的结果,如果文中有不对的地方,请各位数据库大牛给予指正,以便我能够更好的处理此次业务. 项目背景 这是给某数据中心做的一个项目,项目难度之大令人发指,这个项目真正的让我感觉到了,商场如战场,而我只是其中的一个小兵,太多的战术,太多的高层之间的较量,太多的内幕了.具体这个项目的情况,我有空再写相关的博文出来. 这个项目是要求做环境监控,我们暂且把受监控的设备称为采集设备,采集设备的属性

为何在查询中索引未被使用 (Doc ID 1549181.1)

* 为何在查询中索引未被使用 (Doc ID 1549181.1) To Bottom 文档内容 用途   排错步骤   快速检查   表上是否存在索引?   索引是否应该被使用?   索引本身的问题   索引列或者索引的前置列是否在单表(non-join)查询的 Where 条件中(predicate list)?   索引列是否用在连接谓词中(join predicates)?   索引列在 IN 或者多个 OR 语句中?   索引列是否被函数修改?   隐式类型转换(implicit ty

我是如何在SQLServer中处理每天四亿三千万记录的(转)

首先声明,我只是个程序员,不是专业的DBA,以下这篇文章是从一个问题的解决过程去写的,而不是一开始就给大家一个正确的结果,如果文中有不对的地方,请各位数据库大牛给予指正,以便我能够更好的处理此次业务. 项目背景 这是给某数据中心做的一个项目,项目难度之大令人发指,这个项目真正的让我感觉到了,商场如战场,而我只是其中的一个小兵,太多的战术,太多的高层之间的较量,太多的内幕了.具体这个项目的情况,我有空再写相关的博文出来. 这个项目是要求做环境监控,我们暂且把受监控的设备称为采集设备,采集设备的属性

如何在SQLServer中处理每天四亿三千万记录的

项目背景 这是给某数据中心做的一个项目,项目难度之大令人发指,这个项目真正的让我感觉到了,商场如战场,而我只是其中的一个小兵,太多的战术,太多的高层之间的较量,太多的内幕了.具体这个项目的情况,我有空再写相关的博文出来. 这个项目是要求做环境监控,我们暂且把受监控的设备称为采集设备,采集设备的属性称为监控指标.项目要求:系统支持不少于10w个监控指标,每个监控指标的数据更新不大于20秒,存储延迟不超过120秒.那么,我们可以通过简单的计算得出较理想的状态——要存储的数据为:每分钟30w,每个小时

(转)我是如何在SQLServer中处理每天四亿三千万记录的

首先声明,我只是个程序员,不是专业的DBA,以下这篇文章是从一个问题的解决过程去写的,而不是一开始就给大家一个正确的结果,如果文中有不对的地方,请各位数据库大牛给予指正,以便我能够更好的处理此次业务. 项目背景 这是给某数据中心做的一个项目,项目难度之大令人发指,这个项目真正的让我感觉到了,商场如战场,而我只是其中的一个小兵,太多的战术,太多的高层之间的较量,太多的内幕了.具体这个项目的情况,我有空再写相关的博文出来. 这个项目是要求做环境监控,我们暂且把受监控的设备称为采集设备,采集设备的属性