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

1. 值类型

  • 常见的值类型:int/long/short/byte/float/double/bool/char/Struct(用户建立的结构体通常是值类型的)/Nullable Types(这是一个特殊的值类型,表示一个正常值或者空,比如int?)
  • 值类型的例子:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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#中,你需要获取引用类型实例的地址只需要用&,如下
    1
    2
    string str = "Hi"
    IntPtr address = new IntPtr(&str)
  • 但是当想要在值类型实例上获得地址,就变得很困难,你可能需要先把该值类型封装在一个引用类型(比如元素是值类型的数组类型)中,然后再获取该引用类型的地址。
  • 在传参时使用ref,如下,ref实际上传入的不是值类型的值(副本),而是值类型的引用(地址):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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. 由值类型和引用类型不同引发的问题案例

  • 如下,有一个方法,方法尝试把一个字符串切分后的值准确的赋值给结构体:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
        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的时候程序会去扫描并使用该结构体构造函数中的初始值
  • 下面这句话用于调用该方法:
    1
    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():用于获取结构体字段的引用,底层特性,不建议直接使用
      1
      typeof(T).GetFields()[i].SetValueDirect(__makeref(result),parsedValue)
  • 通过GitHub上参考的代码, 找到一个更合适的解决方法,就是创建一个Clone,再把Clone赋值回去,如下:
    • 注意此方法生成的object clone_result,它是一个object类型,引用类型。那就不存在上面值类型放进SetValue()里的那些问题。最后再强转回T,给到result输出。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      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可以成功解决该问题。

浅谈C sharp中的值类型和引用类型
http://example.com/2024/08/27/浅谈C sharp中的值类型和引用类型/
作者
xiao cuncun
发布于
2024年8月27日
许可协议