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 rwdim0()
(或者不使用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上了,如果还是不行,可以考虑给结构体字段都加上
public
。
- 这种用法就能正确得到字典的值并输出到richtextbox上了,如果还是不行,可以考虑给结构体字段都加上
//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
约束泛型 - 值类型和引用类型中间转换出现的错误及原因