存储类型和传递方式

变量的存储类型

csharp 中的存储类型有两种, "值存储" 和 "引用存储".

  1. 引用存储类型是指在堆中存储变量的实际内容, 在栈中存储指向堆中实际内容的指针. 引用储存类型由这两部分组成.

  2. 值存储类型则是直接存储变量的实际内容, 不保存指向其的指针, 具体存储位置根据值类型的创建位置而定: 如果值类型是在一个方法中创建的, 那么它将跟随方法被压入栈内存中, 如果值类型是在一个引用类型内部创建的, 那么它将跟随这个引用类型存储在堆内存中.

参数的传递方式

csharp 中的传递方式有两种, "值传递" 和 "引用传递".

  1. 引用传递就是指将变量本身直接作为参数传递到方法内部. 比如你买了一支冰激凌, 朋友是只馋猫, 于是你直接把冰激凌送给了她, 她吃完后, 你也就没有冰激凌了...

  2. 值传递就是指将变量复制一份, 然后将复制出来的副本传递到方法内部. 就像朋友也想吃的时候, 你去便利店又买了一支一模一样的冰激凌送给了她, 她吃完后, 你的冰激凌还是在自己手中的...自己的是自己的, 她的是她的.

在默认情况下, CLR方法中传递参数的方式都是值传递! 即使变量采用的是引用存储.

四种 "存储--传递"

2 种存储类型在 2 种传递方式下就会诞生 4 种情况:

值存储--值传递

将一个值存储变量按照默认的传递方式传递就构成了 "值存储--值传递" 的情况.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System;

class Program
{
static void Main()
{
Program program = new Program();

int param1 = 1;
int param2 = 2;
program.Edit(param1, param2);

Console.WriteLine("param1={0}, param2={1}", param1, param2);
Console.ReadKey();
}

private void Edit(int param1, int param2)
{
param1 = 3;
param2 = 4;
}
}

在上面的例子中, 最终输出的还是:

1
param1=1, param2=2

引用存储--值传递

将一个引用存储的变量使用默认的传递方式传递就构成了 "引用存储--值传递" 的情况.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System;

class Program
{
static void Main()
{
Program program = new Program();

string str = "我是测试字符串!";

program.Edit(str);

Console.WriteLine("str={0}", str);
Console.ReadKey();
}

private void Edit(string str)
{
str = "我被修改了!";
}
}

在上面的例子中, 最终输出的还是:

1
str=我是测试字符串!
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
using System;

class Program
{
static void Main()
{
Program program = new Program();

Car car = new Car(1000, "价值一千元的车");

program.Edit(car);

Console.WriteLine("price={0}, name={1}", car.price, car.name);
Console.ReadKey();
}

private void Edit(Car param)
{
param = new Car(10000, "价值一万元的车");
}
}

class Car
{
public int price;
public string name;

public Car(int param1, string param2)
{
price = param1;
name = param2;
}
}

在上面的例子中, 最终输出的还是:

1
price=1000, name=价值一千元的车

第一个例子中 param1 的值是 1, param2 的值是 2, 即使使用了 Edit() 方法进行修改, 这两个变量的值依旧没有变化; 第二个例子中, str 的值也没有发生改变; 第三个例子中, car 指向的是 "价值为一千元的车", 之后使用了 Edit() 方法进行了修改, 但是 car 指向的还是那辆 "价值为一千元的车", 对一万元的车视而不见! 由此可见, 值传递之后, 方法中修改的只是变量的副本, 并不会对原变量的值造成任何影响.

[] 第二个例子中的 car 变量的值和 car 所指向的 price 变量的值, name 变量的值完全是两码事!

值存储--引用传递

之前提到过, 在默认情况下, CLR方法中传递参数的方式都是值传递! 即使变量采用的是引用存储! csharp 既然提供了引用传递方式, 自然有其实现方式. ref, in, out 这三个修饰符就是用于修饰参数的, 3 个修饰符的差异先放一边, 只要知道被它们修饰后的参数将使用 "引用传递" 的方式进行传递就可以了.

当值存储的变量作为参数传递时, 被 ref, out, in 修饰符修饰, 会使用引用传递方式进行传递, 就构成了 "值存储--引用传递" 的情况.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System;

class Program
{
static void Main()
{
Program program = new Program();

int param1 = 1;
int param2 = 2;
program.Edit(ref param1, ref param2);

Console.WriteLine("param1={0}, param2={1}", param1, param2);
Console.ReadKey();
}

private void Edit(ref int param1, ref int param2)
{
param1 = 3;
param2 = 4;
}
}

在上面的例子中, 最终输出的值就变成了:

1
param1=3, param2=4

引用存储--引用传递

当引用存储的变量作为参数传递时, 被 ref, out, in 修饰符修饰, 也会使用引用传递方式进行传递, 就构成了 "引用存储--引用传递" 的情况.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System;

class Program
{
static void Main()
{
Program program = new Program();

string str = "我是测试字符串!";

program.Edit(ref str);

Console.WriteLine("str={0}", str);
Console.ReadKey();
}

private void Edit(ref string str)
{
str = "我被修改了!";
}
}

在上面的例子中, 最终输出的值就变成了:

1
str=我被修改了!
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
using System;

class Program
{
static void Main()
{
Program program = new Program();

Car car = new Car(1000, "价值一千元的车");

program.Edit(ref car);

Console.WriteLine("price={0}, name={1}", car.price, car.name);
Console.ReadKey();
}

private void Edit(ref Car param)
{
param = new Car(10000, "价值一万元的车");
}
}

class Car
{
public int price;
public string name;

public Car(int param1, string param2)
{
price = param1;
name = param2;
}
}

在上面的例子中, 最终输出的值就变成了:

1
price=10000, name=价值一万元的车

第一个例子中 param1 的值是 1, param2 的值是 2, 使用了 Edit() 方法修改后, 这两个变量的值就变成了 3 和 4; 第二个例子中, str 的值也被修改为了 "我被修改了!"; 第三个例子中, car 原本指向的是 "价值为一千元的车", 之后使用了 Edit() 方法进行了修改, car 便指向了 "价值为一万元的车" ! 由此可见, 引用传递之后, 方法中修改的就是原变量的值.

ref, out, in

这三个关键字都可以实现引用传递, 并且引用传递时, ref 和 out 还要求不仅需要在方法签名中声明参数为哪种引用传递, 在调用方法的时候也必须添加对应的修饰符, 从上面引用传递的举例中也可以看出, 在调用方法时, 参数中也必须注明 ref 和 out, 但是这三者有什么区别呢?

什么时候用 ref ?

当你的目的是使用方法处理变量的值的时候, 就可以使用 ref 修饰符了. 因此 ref 有一个这样的语法规则:

  • ref 修饰的参数在传递前必须已经初始化了.

很明显, 这条规则进一步强调了 ref 的运用场景是: 我已经有一吨苹果了, 我现在需要将苹果送入造酒工厂, 让造酒工厂帮我处理这么多苹果! 所以前提是我们必须得先有苹果啊! 😅

什么时候用 out ?

当你的目的是使用方法造出一个东西的时候, 就可以使用 out 修饰符了. 因此 out 修饰符就没有必须先初始化的限制. 这也是其使用场景决定的: 比如现在造酒工厂已经把苹果酒制作好了, 那么我只需要将酒带回家就可以了, 并不需要提前准备什么.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System;

class Program
{
static void Main()
{
Program program = new Program();

program.Cook(out string food);

Console.WriteLine("food={0}", food);
Console.ReadKey();
}

private void Cook(out string food)
{
food = "早餐";
}
}

比如上面的例子, Cook() 方法中的 food 变量并没有初始化, 依旧可以正常输出:

1
food=早餐

in 修饰符又有什么用呢?

由上面可以就看出, ref 和 out 已经可以涵盖所有的情况了, 为什么还有一个 in 呢? in 自然也是有它的应用场景的.

某一天你买了一幅名画, 十分贵重, 几乎花光了你至今为止所有的积蓄, 但是你的好朋友也想要看一下, 提出想要拿到自己家中好好观摩两天. 此时你心想, 使用 "值传递" 的方式吧, 就得复制一副相同的画, 虽然朋友也说了就算是复制品也可以, 但是请人再去临摹一份一模一样的, 这个开销太大了(对应程序中传递一个体积很大很大的值存储变量, 比如结构体变量, 复制一份的话, 内存占用就很高), 于是你还是决定直接将原画借给朋友看, 也就是 "ref 引用传递", 但是心里始终是不放心啊, 万一朋友不小心把画作弄脏了, 弄丢了, 那自己这一辈子岂不凉凉了~

于是在这个场景下就可以使用修饰符 in 了, 使用 in 修饰的引用传递, 在方法中只能使用参数值, 无法修改参数值. 这样就不用担心画作的安全问题了.

in 的作用

[] ref 和 out 参数在方法调用时必须显式注明 ref 和 out, 否则编译器直接报错! 但是从上图中可以看出, 我在调用 Friend 方法的时候并没有显式注明 in, 也就是说 in 并没有这个要求, 但是还是建议养成注明引用传递的习惯!

由此可见, 在程序中, in 关键字主要用于程序优化, 节省内存. 当然这里说的 in 只是参数修饰符 in, 自然不包括在 foreach 中的 in 啦~

params

使用 params 关键字可以指定采用数目可变的参数的方法参数, 因此当方法需要的参数个数无法确定的时候, 就可以使用 params 关键字.

params 使用时具有诸多语法限制:

  1. params 修饰的参数类型必须是一维数组. 如果 params 修饰的不是一维数组, 直接发生编译错误;

  2. 在方法声明中 params 关键字修饰的参数之后不允许有任何其他参数;

  3. 在方法声明中只允许有一个 params 关键字.

调用具有 params 修饰参数的方法时,可以传入:

  1. 一维数组元素类型的逗号分隔列表;

  2. 指定类型的一维数组;

  3. 无参数. 如果未发送任何参数, 则 params 列表长度为零.

[]

  1. params 修饰的参数类型必须是一维数组, 但是这里的 "一维数组" 指代的并不仅仅是这种简单的一维数组 int[], 任何类型的一维数组都可以, 包括交错数组: int[][].

  2. 在方法声明中的 params 关键字之后不允许有任何其他参数, 就意味着 params 修饰的参数必须放在参数列表的最后一个, 同时也意味着只允许有一个 params 修饰的参数.

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
using System;
using System.Collections.Generic;

class Program
{
static void Main()
{
Program program = new Program();

List<string> appleList = new List<string>();
appleList.Add("富士苹果");
appleList.Add("红龙苹果");
appleList.Add("香蕉苹果");
//......
appleList.Add("蜜脆苹果");

string[] apples = appleList.ToArray();

program.Factory();
program.Factory("富士苹果", "红龙苹果");
program.Factory("富士苹果", "红龙苹果", "香蕉苹果", "蜜脆苹果");
program.Factory(apples);

Console.ReadKey();
}

private void Factory(params string[] apples)
{
string str_log = string.Empty;

if (apples.Length > 0)
{
foreach (string apple in apples)
{
str_log += apple + "酒! ";
}
}
else
{
str_log = "请提供原材料!";
}

Console.WriteLine(str_log);
}
}

上面的例子输出的结果就是:

1
2
3
4
请提供原材料!
富士苹果酒! 红龙苹果酒!
富士苹果酒! 红龙苹果酒! 香蕉苹果酒! 蜜脆苹果酒!
富士苹果酒! 红龙苹果酒! 香蕉苹果酒! 蜜脆苹果酒!

可以看出调用方法时, 传递不定个数的参数以及一维数组都是可以的!

参考链接

Microsoft Docs ref

Microsoft Docs params