浅谈C#中的值类型和引用类型

1. 值类型

  • 常见的值类型:int/long/short/byte/float/double/bool/char/Struct(用户建立的结构体通常是值类型的)/Nullable Types(这是一个特殊的值类型,表示一个正常值或者空,比如int?)
  • 值类型的例子:
int a=10;
int b=a;

Console.WriteLine($"a:{a}");//a:10
Console.WriteLine($"b:{b}");//b:10

b=20;

Console.WriteLine($"a:{a}");//a:10,原始值不受影响
Console.WriteLine($"b:{b}");//b:20,只有b的值改变了
  • 值类型直接存储在内存(称之为栈(STACK),栈以LIFO访问,后进栈的数据先被访问,栈的大小是固定的,不是动态分配的,所以访问速度快)中,当把一个值赋值给另外一个变量时,其实是把变量的值复制给了新的变量,而不会改变原有值(a=10;b=a;b=20;这个例子中并不会因为b变成20了就反过来使a也变成20了,因为这个过程是复制,副本虽然变了,但是a=10这个原始值一直没有被改变。)
  • 当一个方法传递值类型的参数时(包括结构体),会将参数的值复制到函数的参数中,对参数的修改不会影响到原始变量;

2. 引用类型

  • 类,接口,委托,数组,字符串(字符串比较特殊,他可以像值类型一样用,但是它又具有不可变性。)
  • 用new动态分配内存,由GC(垃圾回收器)释放
  • 引用类型实际上操作的是地址。在C#中,你需要获取引用类型实例的地址只需要用&,如下
string str = "Hi";
IntPtr address = new IntPtr(&str)
  • 但是当想要在值类型实例上获得地址,就变得很困难,你可能需要先把该值类型封装在一个引用类型(比如元素是值类型的数组类型)中,然后再获取该引用类型的地址。
  • 在传参时使用ref,如下,ref实际上传入的不是值类型的值(副本),而是值类型的引用(地址):
public static void ModifyValue(ref int num)
{
  num = 42;
}
public static void Main()
{
  int value = 10;
  Console.WriteLine(Value);// output 10

  ModifyValue(ref value);
  Console.WriteLine(value);// output 42
}
  • 引用类型存储在内存的堆(Heap),动态分配,当在堆上分配了实例之后(new之后),访问该实例实际上是通过访问该实例的内存地址来访问该实例的。

3. 由值类型和引用类型不同引发的问题案例

  • 如下,有一个方法,方法尝试把一个字符串切分后的值准确的赋值给结构体:
    public static T ParseString2Struct<T>(string in_str) where T : struct
    {
        T result = default(T);
        //Type type = result.GetType();
        //object clone_result = Activator.CreateInstance(type);

        FieldInfo[] fields = typeof(T).GetFields(BindingFlags.Instance | BindingFlags.Public);
        object parsedValue;

        var lines = in_str.Split(new[] { "\n" }, StringSplitOptions.RemoveEmptyEntries);
        if (lines.Length != fields.Length)
        {
            throw new ArgumentException("richtextbox string length does not match the structer.");
        }

        for (int i = 0; i < fields.Length; i++)
        {
            var lineParts = lines[i].Split(':');
            if (lineParts.Length != 2 || lineParts[0].Trim() != fields[i].Name)
            {
                throw new ArgumentException("richtextbox string format does not match the structer.");
            }
            var value = lineParts[1].Trim();
            var fieldType = fields[i].FieldType;

            try
            {
                parsedValue = Convert.ChangeType(value, fieldType);
            }
            catch (Exception)
            {
                throw new ArgumentException("richtextbox string does not match the structer.");
            }
            if (!fields[i].IsInitOnly)
            {
                fields[i].SetValue(result, parsedValue);
            }          
        }
        //result = (T)clone_result;
        return result;
    }
}
  • 在这个方法里,使用T result = default(T);初始化值类型,这里不管是使用default(T)还是new T()其实本质都是获取了一个全新的值类型副本,但是default和new之间有一些小区别:用default的时候不会去获取结构构造函数中的初始值,而是直接使用该字段的数据类型的默认初始值。用new的时候程序会去扫描并使用该结构体构造函数中的初始值
  • 下面这句话用于调用该方法:
Ds64_65 ds6465 = DataSet_lib.ParseString2Struct<Ds64_65>(str);
  • 实际调试中发现一个很奇怪的现象,不管parsedValue值是多少,result的字段无论怎样都不能被赋值:
    • FieldInfo[] fields = typeof(T).GetFields(BindingFlags.Instance | BindingFlags.Public);用来设定结构体字段的public属性。
    • fields[i].IsInitOnly用来检查每个字段都没有设置只读属性。
    • 为了检查FieldInfo[]类中的方法是否适用,甚至通过修改结构体构造函数的初始值再用new T()创建一个新的初始结构体,然后使用GetValue()方法,可以正常获得初始化值。
  • 通过询问AI才知道SetValue()方法对于值类型的注意点:
    • 1.值传递是传递的字段的副本,副本的改变对字段本身的值没有影响。
    • 2.传递的值和字段类型是兼容的,不兼容会抛异常,这一条上面的方法可以确保。
    • 3.对于结构体中的字段,SetValue()方法只会修改字段的副本,而不是原始结构体实例。意味着在使用SetValue()修改结构体之后,需要将修改后的副本重新赋值给原始结构体实例.
  • 但SetValue()这个方法本身又没有包含ref类型的重载,所以不能靠ref。
  • 对于修改建议,AI建议是使用SetValueDirect:
    • __makeref():用于获取结构体字段的引用,底层特性,不建议直接使用
typeof(T).GetFields()[i].SetValueDirect(__makeref(result),parsedValue)
  • 通过GitHub上参考的代码, 找到一个更合适的解决方法,就是创建一个Clone,再把Clone赋值回去,如下:
    • 注意此方法生成的object clone_result,它是一个object类型,引用类型。那就不存在上面值类型放进SetValue()里的那些问题。最后再强转回T,给到result输出。
 public static T ParseString2Struct<T>(string in_str) where T : struct
 {
     T result = default(T);
     Type type = result.GetType();
     object clone_result = Activator.CreateInstance(type);

     FieldInfo[] fields = typeof(T).GetFields(BindingFlags.Instance | BindingFlags.Public);
     object parsedValue;

     var lines = in_str.Split(new[] { "\n" }, StringSplitOptions.RemoveEmptyEntries);
     if (lines.Length != fields.Length)
     {
         throw new ArgumentException("richtextbox string length does not match the structer.");
     }

     for (int i = 0; i < fields.Length; i++)
     {
         var lineParts = lines[i].Split(':');
         if (lineParts.Length != 2 || lineParts[0].Trim() != fields[i].Name)
         {
             throw new ArgumentException("richtextbox string format does not match the structer.");
         }
         var value = lineParts[1].Trim();
         var fieldType = fields[i].FieldType;

         try
         {
             parsedValue = Convert.ChangeType(value, fieldType);
         }
         catch (Exception)
         {
             throw new ArgumentException("richtextbox string does not match the structer.");
         }
         if (!fields[i].IsInitOnly)
         {
             fields[i].SetValue(clone_result, parsedValue);
         }          
     }
     result = (T)clone_result;
     return result;
 }
  • Clone可以成功解决该问题。
posted @ 2024-08-23 16:25  你要去码头整点薯条吗  阅读(116)  评论(0)    收藏  举报