C#不完全折腾:结构体,值类型,引用类型,泛型

1. 背景

  • 需要创建多个不同结构的结构体,依据输入的int数来选择并返回确定且唯一的结构体实例,并且实现把结构实例映射成一个字典(键值对)格式用来输出到richtextbox.

2. 结构体的初始化

  • 一个最基本的结构体格式如下:
//rwd im0
public struct rwdim0
{
    public ushort blocktype;
    public short blocklen;
    public ushort blockversion;
    public ushort manufactureID;
    public char[] order_num;//length 20
    public char[] serial_num;//length 16
    public short hardware_revision;
    public char fw_profix;
    public byte fw_cartridge;
    public byte fw_bugfix;
    public byte fw_internal_change;
    public short revirsion_counter;
    public short profil_ID;
    public short profil_specific_type;
    public ushort im_version;
    public ushort im_supported;
}
  • 需要用这个结构体的时候,直接rwdim0 my_im0;就能开辟一个栈内存(或者使用new,调用构造函数)。当结构体里面有数组的时候,不能在声明变量的时候就初始化, 如下:
//rwd im0
public struct rwdim0
{
    public char[] order_num;//length 20
    public char[] serial_num;//length 16
}
  • 如果需要在结构体定义阶段就初始化长度,只需要在创建结构体的同时创建一个无参构造函数即可
    • 可以把初始值写在构造函数里
    • 也可以把初始值写在结构体的字段里,此时该结构体必须包含一个空的构造函数
    • 如果省略空的构造函数却把初值给到结构体字段内:此时会出现错误:CS8983:具有字段初始值设定项的结构必须包含显示声明的构造函数
        //初始化写在构造函数
        public struct im0
        {
            public char[] order_num;//length 20
            public char[] serial_num;//length 16
            public byte fw_bugfix;

            public im0()
            {
                order_num = new char[20];//char是值类型,默认的值是\0 而不是null;
                serial_num = new char[16];
                fw_bugfix = 0;//byte是值类型,默认的值是0, 而不是null
            }
        }
        //初始化写在结构体字段内,此时有一个空的构造函数
        public struct im0
        {
            public char[] order_num = new char[20];//length 20
            public char[] serial_num = new char[16];//length 16
            public byte fw_bugfix = 0;
            public im0()
            {

            }
        }

3. 实现返回多种结构的方法

  • 这个实现看似很简单,好像用一个switch..case语句就能解决,但是在考虑如何接收返回值的时候,真正的难题才开始。
  • 如下:首先考虑用object接收:
//get struct
public static object GetStructInstance(int index_num)
{
    switch (index_num)
    {
        case 0:
        case 1:
        case 2:
        case 3:
            return new Ds0_1_2_3();
        case 64:
        case 65:
            return new Ds64_65();
        case 128:
            return new Ds128();
        case 192:
        case 193:
        case 194:
        case 195:
            return new Ds192_193_194_195();
        default:
            throw new ArgumentOutOfRangeException(nameof(index_num), "invalid index number");
    }
}
  • 调用语句如下,用一个object类型的results接收这个可变的结果:
            //read data
            object results = DataSet_lib.GetStructInstance(Convert.ToInt16(DS_index_textbox.Text.ToString()));
  • 看似接收没有问题,但是要把results用作后续结构体示例变成字典实例的时候却犯了难,如下:是结构体实例变字典实例的实现方法:
    • 这里用到了泛型<T>,因为要求的输入是一个结构体,所以用了where T : struct来做约束
    • 在使用了where T : struct但是传入的参数却是一个object类型的时候,会出现一个很典型的错误:CS0453: 类型object必须是不可为null值的类型,才能用作泛型类型或者方法...中的参数T。这个错误表达了:因为object类型是引用类型,但是struct类型却是一个值类型,引用类型有null值,值类型没有null值,所以不能随意转换。
        //struct 2 dictionary
        public static Dictionary<string, object> Struct2Dictionary<T>(T struc) where T : struct
        {
            //正常执行
            Dictionary<string, object> dict = new Dictionary<string, object>();
            var fields = typeof(T).GetFields();

            foreach (var field in fields)
            {
                dict.Add(field.Name, field.GetValue(struc));
            }
            return dict;
        }
    }
  • 为了解决CSO453的问题,我把where T : struct约束给删了,此时不报错了,但是运行的时候richtextbox一直拿不到值,也没有报错,百思不得其解,于是尝试用调试模式看一看。
    • 选择方法所在的事件,选中该行并右键,选择运行到光标处,随后按F11一步一步看数据。
    • 在进入Struct2Dictionary方法之前,需要传入的result已经能得到值了(通过调试模式看到的值),但是在Struct2Dictionary方法中,var fields = typeof(T).GetFields();这条语句最后拿到的fields一直是空的,出现一个红色提示:{System.Reflction.FieldInfo[0]},这表示反射出来的字段是空的。(另外此处我用的var 推断类型,其实实际类型应该是FieldInfo[],看反射出的类型就知道了。)
    • 为了解决这个问题,我先尝试在使用results之前先判断一下results里面的字段是不是公有的(否则有可能反射不出来),如下写了一个判断方法:
    • 方法结果是好的,可以正常判断数据是公共的,也能正常拿到返回值,但是正常的返回值一进Struct2Dictionary方法的var fields = typeof(T).GetFields();语句就只能得到{System.Reflction.FieldInfo[0]};又开始卡住了。
        //object 2 public object
        public static object ConvertResults(object results)
        {
            if (results != null)
            {
                Type resultsType = results.GetType();
                if (resultsType.IsNotPublic)
                {
                    object publicResults = Activator.CreateInstance(resultsType);
                    foreach (var field in resultsType.GetFields())
                    {
                        field.SetValue(publicResults, field.GetValue(results));
                    }
                    return publicResults;
                }
            }
            return results;
        }
  • 为了解决{System.Reflction.FieldInfo[0]},我甚至把var fields = typeof(T).GetFields();按照如下改动妄图扩大范围,结果依旧无济于事:
var fields = typeof(T).GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
  • 最终只能放弃从object->struct->dictionary的路线,如果有大佬知道问题出在哪里的话,希望可以帮我解答一下,感激不尽

4. 泛型,返回不同的结构体实例

  • 上面的坑踩了没走出来,于是舍弃返回object类型,如下,把GetStructInstance()方法改为返回一个泛型实例:
    • return (T)(object)是因为:结构体实例不能直接转换为泛型T,强转为object类的结构体也不能隐式转换为T,所以经过了两个显示转换。另外,泛型T是值类型还是引用类型取决于实际参数。
    • 泛型在创建的时候不必指定其具体的类型,但是调用的时候必须明确当前的具体类型。
 //get struct
 public static T GetStructInstance<T>(int index_num)
 {
     switch (index_num)
     {
         case 0:
         case 1:
         case 2:
         case 3:
             return (T)(object)new Ds0_1_2_3();
         case 64:
         case 65:
             return (T)(object)new Ds64_65();
         case 128:
             return (T)(object)new Ds128();
         case 192:
         case 193:
         case 194:
         case 195:
             return (T)(object)new Ds192_193_194_195();
         default:
             throw new ArgumentOutOfRangeException(nameof(index_num), "invalid index number");
     }
 }
  • 这种用法避免了直接返回object类型,返回的是已经确定好的类型,缺点是调用上需要写的代码量增加,不够灵活,如下:
    • 这种用法就能正确得到字典的值并输出到richtextbox上了。对于总是返回null的情况,考虑给结构体字段都加上public来允许反射。
//read data
switch (Convert.ToInt16(DS_index_textbox.Text.ToString()))
{
    case 0:
    case 1:
    case 2:
    case 3:
        Ds0_1_2_3 ds0_1_2_3 = DataSet_lib.GetStructInstance<Ds0_1_2_3>(Convert.ToInt16(DS_index_textbox.Text.ToString()));
        my_ds_dic = DataSet_lib.Struct2Dictionary(ds0_1_2_3);
        break;
    case 64:
    case 65:
        Ds64_65 ds6465 = DataSet_lib.GetStructInstance<Ds64_65>(Convert.ToInt16(DS_index_textbox.Text.ToString()));
        my_ds_dic = DataSet_lib.Struct2Dictionary(ds6465);
        break;
    case 128:
        Ds128 ds128 = DataSet_lib.GetStructInstance<Ds128>(Convert.ToInt16(DS_index_textbox.Text.ToString()));
        my_ds_dic = DataSet_lib.Struct2Dictionary(ds128);
        break;
    case 192:
    case 193:
    case 194:
    case 195:
        Ds192_193_194_195 ds192_195 = DataSet_lib.GetStructInstance<Ds192_193_194_195>(Convert.ToInt16(DS_index_textbox.Text.ToString()));
        my_ds_dic = DataSet_lib.Struct2Dictionary(ds192_195);
        break;
    default:
        throw new ArgumentOutOfRangeException("invalid index number");
}
foreach (var ds in my_ds_dic)
{
    readdata_richtextbox.AppendText($"{ds.Key}:{ds.Value}\n");
}

5. 总结

  • 折腾了一天,总结一下哪些有用的
    • 结构体的初始化方法
    • object类型的使用,var类型的使用
    • 泛型的使用
    • 运行到光标处F11调试程序
    • where T : xxx约束泛型
    • 值类型和引用类型中间转换出现的错误及原因
posted @ 2024-08-20 13:06  你要去码头整点薯条吗  阅读(78)  评论(0)    收藏  举报