传参

c# 和 c++ 感觉很不同的一点就是关于 new 的使用,在 c++ 中,new 只是动态分配一块内存,比如 Person p(10,"zhangqr");Person *p = new Perspn(10,"zhangqr"); 前者 p 就是一个实体,后者 p 只是一个指向实体的指针。而在 c# 中,Person p; 表示申明了一个指向 null 的 Person 类型指针,想要在申明时就初始化需要使用 Person p = new Person(10,"zhangqr");,这里的 p 本质上是一个指针。

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
using System;

namespace csharpTest
{
class Product
{
private static int constructTimes = 0;
public Product(string name, int newID)
{
ItemName = name;
ItemID = newID;
Console.WriteLine("product 被构造{0}次, name:{1},id:{2}", ++constructTimes,this.ItemName,this.ItemID);
}
public Product( Product product)
{
Console.WriteLine("product 拷贝构造函数被调用一次");
}

public string ItemName { get; set; }
public int ItemID { get; set; }
}

// This method displays the following output:
// Original values in Main. Name: Fasteners, ID: 54321
// Back in Main. Name: Stapler, ID: 12345
class Program
{
private static void ChangeByReference(/*ref*/ Product itemRef)
{
// Change the address that is stored in the itemRef parameter.
itemRef = new Product("Stapler", 99999);

// You can change the value of one of the properties of
// itemRef. The change happens to item in Main as well.
itemRef.ItemID = 12345;
}

private static void ModifyProductsByReference()
{
// Declare an instance of Product and display its initial values.
Product item = new Product("Fasteners", 54321);
System.Console.WriteLine("Original values in Main. Name: {0}, ID: {1}\n",
item.ItemName, item.ItemID);

// Pass the product instance to ChangeByReference.
ChangeByReference(/*ref*/ item);
System.Console.WriteLine("Back in Main. Name: {0}, ID: {1}\n",
item.ItemName, item.ItemID);

// Product item2 = new Product(item);
}
static void Main(string[] args)
{
ModifyProductsByReference();
}
}
}

运行结果:

product 被构造1次, name:Fasteners,id:54321
Original values in Main. Name: Fasteners, ID: 54321

product 被构造2次, name:Stapler,id:99999
Back in Main. Name: Fasteners, ID: 54321

解释:

在函数传递的时候拷贝构造函数也没有被调用,这一点也跟 c++ 不一样,但理解了其实传递的是指针之后就会明白,其实还是一样的,拷贝构造是在用一个实例创建另一个实例的时候会调用,但其实从图中可以看出来从头到尾只有一个实例,增加的也只是指针而已。

如果将 itemRef = new Product("Stapler", 99999);注释掉的话,运行结果如下,这个从图中也很好理解,最后就是两个指针指向同一个实例。

product 被构造1次, name:Fasteners,id:54321
Original values in Main. Name: Fasteners, ID: 54321

Back in Main. Name: Fasteners, ID: 12345

将两个 ref 的注释去掉之后,运行结果为:

product 被构造1次, name:Fasteners,id:54321
Original values in Main. Name: Fasteners, ID: 54321

product 被构造2次, name:Stapler,id:99999
Back in Main. Name: Stapler, ID: 12345

解释:

当传参的时候申明了 ref ,我是将其理解成 c++ 的引用,也操作的是同一个指针,但是有两个不同的名字。

以上并未加验证,本来是想通过 unsafe 来取地址验证一下,但是 unsafe 需要在声明时就加上 * ,也就是像 c++ 的写法一样,而我是想用 c# 的写法来取到地址,所以暂时没找到验证方法,因为目前猜测都合理,所以暂且这么理解。

readonly 与 ref

先上实验代码

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ReadonlyTest : MonoBehaviour
{
public void OnGUI()
{
if(GUILayout.Button("测试 readonly "))
{
// 使用声明 readonly 成员变量时赋的值
ReadOnlyData data = new ReadOnlyData();
Debug.LogFormat("使用申明时的赋值,value types:{0},reference types:a={1},b={2}", data.age,data.refType.a,data.refType.b);

// 使用构造函数给 readonly 成员变量赋值
ReadOnlyDataB dataB = new ReadOnlyDataB { a = 15, b = 25 };
data = new ReadOnlyData(15, ref dataB);
Debug.LogFormat("使用构造函数中的赋值,value types:{0},reference types:a={1},b={2}", data.age,data.refType.a, data.refType.b);

// 改变局部变量的值
Debug.Log("局部变量是否与 reference type 相同(Equals函数)"+ dataB.Equals(data.refType));
dataB.a = 25;
dataB.b = 35;
Debug.LogFormat("改变了局部变量所指的对象,value types:{0},reference types:a={1},b={2},局部变量:a={3},b={4}",
data.age, data.refType.a, data.refType.b,dataB.a,dataB.b);

// 改变 reference type 所指的值
Debug.Log("局部变量是否与 reference type 相同(Equals函数)" + dataB.Equals(data.refType));
data.refType.a = 35;
data.refType.b = 45;
Debug.LogFormat("改变 reference type 所指的对象,value types:{0},reference types:a={1},b={2},局部变量:a={3},b={4}",
data.age, data.refType.a, data.refType.b, dataB.a, dataB.b);

// 改变局部变量的指向
dataB = new ReadOnlyDataB { a = 45, b = 55 };
Debug.Log("改变了局部变量的指向后,是否还与 reference type 相同(Equals函数)" + dataB.Equals(data.refType));
Debug.LogFormat("改变了 readonly reference type 所指的对象,value types:{0},reference types:a={1},b={2}", data.age, data.refType.a, data.refType.b);

// data.ChangeReferenceValue();
// Debug.Log("类内改变 reference type 之后,是否还与 reference type 相同(Equals函数)" + dataB.Equals(data.refType));
// Debug.LogFormat("类内改变 reference type 之后,value types:{0},reference types:a={1},b={2},局部变量:a={3},b={4}",
data.age, data.refType.a, data.refType.b, dataB.a, dataB.b);
// data.refType = new ReadOnlyDataB { a = 45, b = 55 };// 无法对只读字段进行赋值,初始化和构造函数除外。
// data1.age = 20; // 无法对只读字段进行赋值,初始化和构造函数除外。

}
}
}

public class ReadOnlyData
{
// readonly 变量直接在申明的时候赋值。
public readonly int age;

// reference types 用 readonly 修饰
public readonly ReadOnlyDataB refType = new ReadOnlyDataB { a = 10, b = 20 };

public ReadOnlyData(int age,ref ReadOnlyDataB refType)
{
this.age = age; // 可以, readonly 变量可以在构造函数中赋值。
this.refType = refType;
}

public ReadOnlyData()// :this(10,new ReadOnlyDataB { a=10,b=20})
{
}

public void ChangeAge(int age)
{
// this.age = age; // 错误,除了申明和构造函数中不能对 readonly 赋值
}

public void ChangeReferenceValue()
{
// this.refType = new ReadOnlyDataB { a = 45, b = 55 };// 错误,除了申明和构造函数中不能对 readonly 赋值
}
}

public class ReadOnlyDataB
{
public int a;
public int b;
}

运行结果为:

  • 使用声明时的赋值,value types:10,reference types:a=10,b=20
  • 使用构造函数中的赋值,value types:15,reference types:a=15,b=25
  • 局部变量是否与 reference type 相同(Equals函数)True
  • 改变了局部变量所指的对象,value types:15,reference types:a=25,b=35,局部变量:a=25,b=35
  • 局部变量是否与 reference type 相同(Equals函数)True
  • 改变 reference type 所指的对象,value types:15,reference types:a=35,b=45,局部变量:a=35,b=45
  • 改变了局部变量的指向后,是否还与 reference type 相同(Equals函数)False
  • 改变了局部变量的指向后,value types:15,reference types:a=35,b=45,局部变量:a=45,b=55

上面结果都是合理的,然后我在传参的时候将引用类型设置为了 ref,具体是:

  • data = new ReadOnlyData(15, dataB); 改为 data = new ReadOnlyData(15, ref dataB);
  • public readonly ReadOnlyDataB refType; 改为 public readonly ReadOnlyDataB refType = new ReadOnlyDataB { a = 10, b = 20 };
  • public ReadOnlyData() :this(10,new ReadOnlyDataB { a=10,b=20}) 改为 public ReadOnlyData() // :this(10,new ReadOnlyDataB { a=10,b=20})
  • public ReadOnlyData(int age,ReadOnlyDataB refType) 改为 public ReadOnlyData(int age,ref ReadOnlyDataB refType)
    按我的设想,此时 成员变量 refTypeReadOnlyData 的二参 refType局部变量data 应该都是同一个指针,只是有不同的别名而已,所以在 dataB = new ReadOnlyDataB { a = 45, b = 55 }; 之后,三者所指向的实例应该是一样的,结果并不是,运行结果是跟一次完全一样的,在梳理了 ref 的用法之后才惊觉 ReadOnlyData 的二参 refType局部变量data 是同一个指针,但 成员变量 refType 并不是,因为他是直接用赋值符号得到的。验证如下:
    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
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;

    public class ReadonlyTest : MonoBehaviour
    {
    public void OnGUI()
    {
    if(GUILayout.Button("测试 readonly "))
    {
    // 使用声明 readonly 成员变量时赋的值
    ReadOnlyData data = new ReadOnlyData();
    Debug.LogFormat("使用申明时的赋值,value types:{0},reference types:a={1},b={2}", data.age,data.refType.a,data.refType.b);

    // 使用构造函数给 readonly 成员变量赋值
    ReadOnlyDataB dataB = new ReadOnlyDataB { a = 15, b = 25 };
    data = new ReadOnlyData(15, ref dataB);
    Debug.LogFormat("使用构造函数中的赋值,value types:{0},reference types:a={1},b={2}", data.age,data.refType.a, data.refType.b);

    // 改变局部变量的值
    //Debug.Log("局部变量是否与 reference type 相同(Equals函数)"+ dataB.Equals(data.refType));
    //dataB.a = 25;
    //dataB.b = 35;
    //Debug.LogFormat("改变了局部变量所指的对象,value types:{0},reference types:a={1},b={2},局部变量:a={3},b={4}",
    // data.age, data.refType.a, data.refType.b,dataB.a,dataB.b);

    //// 改变 reference type 所指的值
    //Debug.Log("局部变量是否与 reference type 相同(Equals函数)" + dataB.Equals(data.refType));
    //data.refType.a = 35;
    //data.refType.b = 45;
    //Debug.LogFormat("改变 reference type 所指的对象,value types:{0},reference types:a={1},b={2},局部变量:a={3},b={4}",
    // data.age, data.refType.a, data.refType.b, dataB.a, dataB.b);

    // 改变局部变量的指向
    // dataB = new ReadOnlyDataB { a = 45, b = 55 };
    Debug.Log("改变了局部变量的指向后,是否还与 reference type 相同(Equals函数)" + dataB.Equals(data.refType));
    Debug.LogFormat("改变了局部变量的指向后,value types:{0},reference types:a={1},b={2},局部变量:a={3},b={4}",
    data.age, data.refType.a, data.refType.b, dataB.a, dataB.b);

    // data.refType = new ReadOnlyDataB { a = 45, b = 55 };// 无法对只读字段进行赋值,初始化和构造函数除外。
    // data1.age = 20; // 无法对只读字段进行赋值,初始化和构造函数除外。

    }
    }
    }

    public class ReadOnlyData
    {
    // readonly 变量直接在申明的时候赋值。
    public readonly int age = 10;

    // reference types 用 readonly 修饰
    public readonly ReadOnlyDataB refType = new ReadOnlyDataB { a = 10, b = 20 };

    public ReadOnlyData(int age,ref ReadOnlyDataB refType)
    {
    refType = new ReadOnlyDataB { a = 100, b = 101 };
    this.age = age; // 可以, readonly 变量可以在构造函数中赋值。
    this.refType = refType;
    this.refType.a = 120;
    this.refType.b = 121;
    }

    public ReadOnlyData() // :this(10,new ReadOnlyDataB { a=10,b=20})
    {
    }

    public void ChangeAge(int age)
    {
    // this.age = age; // 错误,除了申明和构造函数中不能对 readonly 赋值
    }

    public void ChangeReferenceValue()
    {
    // this.refType = new ReadOnlyDataB { a = 45, b = 55 };// 错误,除了申明和构造函数中不能对 readonly 赋值
    }
    }

    public class ReadOnlyDataB
    {
    public int a;
    public int b;
    }

运行结果:

  • 使用申明时的赋值,value types:10,reference types:a=10,b=20
  • 使用构造函数中的赋值,value types:15,reference types:a=120,b=121
  • 改变了局部变量的指向后,是否还与 reference type 相同(Equals函数)True
  • 改变了局部变量的指向后,value types:15,reference types:a=120,b=121,局部变量:a=120,b=121

解释如图:

将 ref 修饰词给去掉之后:

  • 使用申明时的赋值,value types:10,reference types:a=10,b=20
  • 使用构造函数中的赋值,value types:15,reference types:a=120,b=121
  • 改变了局部变量的指向后,是否还与 reference type 相同(Equals函数)False
  • 改变了局部变量的指向后,value types:15,reference types:a=120,b=121,局部变量:a=15,b=25

解释如图: