C#学习笔记(六)

第6讲方法2

  现在来讲一下方法的传递机制。方法的传递机制分为三种:
  PPT:值参数(Value Parameter)
      方法名称(参数类型 参数名称 [,参数类型 参数名字])
      引用参数(Reference Parameter)
      方法名称(ref 参数类型 参数名称 [,ref 参数类型 参数名字])
      输出参数(Output Paramenter)
      方法名称(out参数类型 参数名称 [,out 参数类型 参数名字])
这三种参数的区别在哪里呢,下面我们用简单的例子来讲述:

  1. using System;
  2. class Method
  3. {
  4.     //首先声明一个函数,给参数i加1
  5.     public static void ValueMethod(int i)
  6.     {
  7.         i++;
  8.     }
  9.     //入口函数
  10.     static void Main()
  11.     {
  12.         //声明一个变量i,初始化为0
  13.         int i = 0;
  14.         //调用函数
  15.         ValueMethod(i);
  16.         Console.WriteLine("i=" + i);
  17.     }
  18. }

我们进行编译代码,看看i的值是多少,如图:

我们看i的值还是0,没有改变。换句话说,虽然i作为ValueMethod的参数传递了进去,在参数内执行了i++,但是这个函数的执行并没有改变i的值,i还是等于0。这意味着Main函数里的局部变量i跟ValueMethod方法里的参数i并不是同一个变量。虽然ValueMethod给i加了 1,但是它并不影响Main函数里局部变量i。接下来我们写个带引用参数的方法。

  1. using System;
  2. class Method
  3. {
  4.     //首先声明一个函数,给参数i加1
  5.     public static void ValueMethod(int i)
  6.     {
  7.         i++;
  8.     }
  9.     //声明一个带引用参数的函数
  10.     public static void ReferenceMethod(ref int i)
  11.     {
  12.         i++;
  13.     }
  14.     //入口函数
  15.     static void Main()
  16.     {
  17.         //声明一个变量i,初始化为0
  18.         int i = 0;
  19.         //调用函数
  20.         ValueMethod(i);
  21.         Console.WriteLine("i=" + i);
  22.         int j = 0;
  23.         //由于引用函数ReferenceMethod的参数前带ref关键字,即引用参数
  24.         //所以在调用时也必须在参数前面加上ref关键字
  25.         ReferenceMethod(ref j);
  26.         //输出j的值
  27.         Console.WriteLine("j=" + j);
  28.     }
  29. }

好,我们再来看看带引用参数方法ReferenceMethod,Main函数中j的值是多少~

我们看j=1,这一次Main函数中局部变量j它的值改变了由于ReferenceMethid这个方法它的参数前面带了个ref关键字,使得它可以直接操作传入的参数j,从而使得j作出改变。下面我们再声明一个带输出参数的方法。

  1. using System;
  2. class Method
  3. {
  4.     //首先声明一个函数,给参数i加1
  5.     public static void ValueMethod(int i)
  6.     {
  7.         i++;
  8.     }
  9.     //声明一个带引用参数的函数
  10.     public static void ReferenceMethod(ref int i)
  11.     {
  12.         i++;
  13.     }
  14.     //声明一个带输出参数的函数
  15.     public static void OutputMethod(out int i)
  16.     {
  17.         //out参数规定参数在方法体内必须被初始化
  18.         //否则会编译出错,大家可以实验下
  19.         i = 0;
  20.         i++;
  21.     }
  22.     //入口函数
  23.     static void Main()
  24.     {
  25.         //声明一个变量i,初始化为0
  26.         int i = 0;
  27.         //调用函数
  28.         ValueMethod(i);
  29.         Console.WriteLine("i=" + i);
  30.         int j = 0;
  31.         //由于引用函数ReferenceMethod的参数前带ref关键字,即引用参数
  32.         //所以在调用时也必须在参数前面加上ref关键字
  33.         ReferenceMethod(ref j);
  34.         //输出j的值
  35.         Console.WriteLine("j=" + j);
  36.         //同样的方法,声明一个变量k
  37.         //这里并不需要对k进行初始化,OutputMethod中已经初始化了        
  38.         int k;
  39.         OutputMethod(out k);
  40.         //输出k的值
  41.         Console.WriteLine("k=" + k);
  42.     }
  43. }

进行编译执行程序,效果如下:

k的值也为1。换句话说使用引用参数和使用out参数所得出的结果都是一样的。下面我们用图例来讲解三种参数的区别:
  首先是值参数的调用。

  在Main函数中声明了一个变量i,这时候在内存的堆栈中就会专门为i开辟出一块新的内存空间,并存放它。接下来执行了ValueMethod方法并把i作为参数传递进去,而在执行ValueMethod方法时,它会在内存堆栈中开辟出另一块区域,并把i的值拷贝过来,赋给方法中的i值。接着就对这个另外开辟内存空间的i进行操作。导致方法中的i被加1,而Main函数中的i并没有做任何的改变,最后一句Console.WriteLine("i="+i)打印i的值自然它还是0。
  接下来我们再来看引用参数的调用。

  在Main函数中声明了变量j,使得堆栈中开辟了一个新的空间来存放j,这时候调用了ReferenceMethod方法,并把j作为参数传递了进去,而由于ref关键字的存在,使得这次传递的是一个内存中的指针。也就是说,这次它会告诉ReferenceMethod方法我传入参数的地址在哪,你直接调用它就行了。随意ReferenceMethod方法直接通过这个指针找到参数i在内存中的地址而去操纵它。也就是说Main函数中的j跟ReferenceMethod函数中的i它所指向的是同一块内存地址,他们的变量是一样的。所以当ReferenceMethod方法中给参数i加1,直接导致了j的变化,因为他们所指向的是同一个内存地址。最后一行打印本地变量j,由于j的值作了改变,所以呢打印出来的是1。
  我们再来看一下输出参数的调用。

  输出参数的调用跟引用参数的调用非常类似,它所传送的也是一块内存地址。而OutputMethod方法中的参数找到这块内存地址,并对它里面的i进行操作。唯一不同的是,在输出参数中必须对参数初始化。
  我们来对原来的代码做些修改,我们将Main函数中的j不进行初始化,而是在ReferenceMethod中将其初始化,我们看看能否通过编译。

不能通过,提示了使用了未赋值的局部变量j。换句话说,在调用ref方法之前必须对参数进行初始化,而不是在方法内部进行初始化。
  输出参数和引用参数的区别:
  从CLR(公共语言运行时)的角度看,关键字out和关键字ref是等效的,这就是说,无论使用哪个关键字,都会生成相同的数据和IL代码(我们知道.NET中使用的是中间代码,无论你使用C#还是VB.NET来编写代码都会生成IL代码)。但是, C#编译器将两个关键字区别对待,在C#中,这两个关键字的区别在于哪个方法负责初始化引用对象。 如果方法的参数标记为out,那么调用者不希望在调用方法之前初始化对象,被调用的方法不能读取对象的值,而且被调用的方法必须在返回之前为对象赋值。如果方法的参数标记为ref,那么调用者必须在调用方法之前首先初始化参数的值,被调用的方法可以读取参数或为参数赋值。
  接下来,我们讲一下向方法传递可变数量的参数。为了将方法声明为可以接受可变数量参数的方法,使用params关键字。代码如下:

  1. using System;
  2. class Method
  3. {
  4.     //首先声明一个可变数量参数的方法
  5.     //在params关键字后面必须跟上一个数组,在这里是一个整形的数组
  6.     //这样它就可以接受多个参数了,并将参数存放在values这个数组里面
  7.     //addi方法的功能是将所有传入的参数相加并返回
  8.     static int addi(params int[] values)
  9.     {
  10.         //声明一个变量sum并设初值为0
  11.         int sum = 0;   
  12.         //用foreach方法遍历数组里每一个值
  13.         foreach (int i in values)
  14.         {
  15.             sum += i;
  16.         }
  17.         return sum;
  18.     }
  19.     static void Main()
  20.     {
  21.         //调用addi方法
  22.         Console.WriteLine(addi(1,2,3,4,5));
  23.     }
  24. }

我们进行编译执行,效果如下:

我们看到屏幕上正确的输出了结果。我们返回来看代码,通过params关键字可以向方法传递可变数量参数。
  下面我们尝试数组传递的方式。代码如下:

  1. using System;
  2. class Method
  3. {
  4.     static void PrintArr(int[] arr)
  5.     {
  6.         for (int i = 0; i < arr.Length; i++)
  7.             arr[i] = i;
  8.     }
  9.     static void Main()
  10.     {
  11.         //声明一个数组并初始化
  12.         int[] arr = { 100, 200, 300, 400 };
  13.         PrintArr(arr);
  14.         foreach (int i in arr)
  15.             Console.Write(i+", ");
  16.     }
  17. }

我们编译代码看下效果,大家可以在之前判断下结果会是什么。效果如下:

打印出来的是0,1,2,3。我们回过头来看下程序,int[] arr = { 100, 200, 300, 400 };arr初始化100,200,300,400。可是经过PrintArr方法的调用以后,它的值改变了,改变成方法里所赋的值。大家可能会觉的奇怪了,PrintArr方法中的参数我也没定义成ref呀,为什么数组的值会改变呢?这是因为数组是一个引用类型的变量,这里我们就来讲一下值类型和引用类型的差别。
  值类型和引用类型:
  类型分为值类型和引用类型。类型区分为这两大类的主要原因是在于执行性能与内存资源管理的不同。由于值类型变量直接在堆栈(stack)中存储该类型的值,此类类型的内存的使用上以及访问的效能上比引用类型更好。因为引用类型变量存放的是指向实际对象的指针,因此访问对象时必须对进行一次内存引用的操作方可获取数据。且引用类型的对象必须分配多余的内存来存放虚函数指针及线程同步块,对于内存的需求较大。而使用引用类型的优点是回收站会自动替你管理分配在托管堆(Managed Heap)当中的内存。
  我们来看下面的图标,清楚的比较它们之间的不同。


值类型引用类型
变量中存放    真正的数据   
指向数据的引用指针
内存空间分配
堆栈(stack)托管堆(Managed Heap)
内存需求一般来说较少
较大
执行效能较快较慢
内存释放时间点
执行超过定义变量的作用域由回收站负责回收
可以为null不可    可以

当然这里还少说了一个值类型的缺点,在特定的条件下值类型需要进行装箱和拆箱的操作,这个会使得它的执行效能变慢。
我们来看下面的图:

值类型,当我们声明一个变量i的时候,就会在堆栈中分配一个新的空间,并把0存放在里面。也就是说值类型变量的值直接存放在堆栈中的。当我们声明一个数组i并把它初始化为1,2,3之后,这3个元素就会被存放在托管堆中,而堆栈中存放的指向这个托管堆的地址。当我们把数组作为参数赋给方法之后,它所传递的其实是堆栈中的地址,而方法接受到这个地址以后,就根据这个地址到托管堆中直接操作这个数组里的元素了。这也是为什么我们把数组作为值参数传递给方法以后他的值仍会改变的原因。
  当然这里也有意外,比如说字符串。字符串也是一个引用类型的变量。下面我们来做一下实验。
  1. using System;
  2. class Method
  3. {
  4.     //首先声明一个操作字符串的方法
  5.     //参数是字符串类型
  6.     static void SetStr(string s)
  7.     {
  8.         //给s进行赋值
  9.         s = "987654";
  10.     }
  11.     static void Main()
  12.     {
  13.         //声明一个字符串变量
  14.         string s = "123456";
  15.         //调用方法SetStr
  16.         SetStr(s);
  17.         //在控制台中打印s
  18.         Console.WriteLine(s);
  19.     }
  20. }

进行编译代码,效果如下:

在这里屏幕上打印的是123456,也就是说Main函数值中s的值并没有被改变,这是为什么呢?s本身是引用类型的变量,它为什么不被改变呢?这是由string对象特点来决定的,string对象最重要的一个事实就是它是不可变的。也就是说,字符串在创建之后就再也不能改变,其中包括变长变短。或者修改其中的任何字符,由于字符串这个特点使得在调用SetStr方法时,SetStr会在堆中创建另一个s的副本来操作它。使得Main函数中的s值并没有改变。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值