C#学习笔记:参数传递(详解)


习惯了C、C++的程序员,也似乎习惯了形参、实参、指针的一些概念,于是在学习C#的时候总会将一些思想与指针混淆,希望通过这篇文章能够让还在参数传递问题上困惑的大家能够有一个清晰的认识。文章内容会略显冗长,高手可以飘过,但如果想弄明白这个问题的,我建议你能耐心的看完~

在谈到参数传递的问题前,必须了解以下两个问题:

一、值类型 与 引用类型
如果对这个概念都分不清楚,建议读者参考:http://www.cnblogs.com/Jason_z/archive/2009/10/13/1582346.html

二、ref 与 out 关键字
使用ref和out关键字,都可以将参数传递的方式设置为引用传递,但两者还有一些区别:
1.使用ref型参数时,传入的参数必须先被初始化。对out而言,必须在方法中对其完成初始化。
2.使用ref和out时,在方法的参数和执行方法时,都要加Ref或Out关键字,以满足匹配。
3.out适合用在需要retrun多个返回值的地方,而ref则用在需要被调用的方法修改调用者的引用的时候。

在C#中,方法的参数传递有四种类型:
传值(by value),
传址(by reference),
输出参数(by output),
数组参数(by array)。
传值参数无需额外的修饰符,传址参数需要修饰符ref,输出参数需要修饰符out,数组参数需要修饰符params。
传值参数在方法调用过程中如果改变了参数的值,那么传入方法的参数在方法调用完成以后并不因此而改变,而是保留原来传入时的值。
传址参数恰恰相反,如果方法调用过程改变了参数的值,那么传入方法的参数在调用完成以后也随之改变。

下面对参数传递的几种情况做下总结:

一、常规类型(以整型数据为例)

值传递

ContractedBlock.gif ExpandedBlockStart.gif Example1
//Example1

using System;

namespace Program
{
    
class Program
    {
        
static void Main(string[] args)
        {
            
int a = 1;
            Console.WriteLine(a); 
//输出1
            Fun(a);
            Console.WriteLine(a); 
//输出1
        }

        
static void Fun(int a)
        {
            a 
= 2;
        }
    }
}

引用传递(使用ref和out)



ContractedBlock.gif ExpandedBlockStart.gif Example2
//Example2

using System;

namespace Program
{
    
class Program
    {
        
static void Main(string[] args)
        {
            
int b=1;//ref引用前,参数必须初始化

            Console.WriteLine(b); 
//输出1

            funWithRef(
ref b);//必须显式的使用关键字ref

            Console.WriteLine(b); 
//输出2

            
int c;//out引用前,参数可以不用初始化

            funWithOut(
out c);//必须显式的使用关键字out

            Console.WriteLine(c);
//输出3
        }

        
static void funWithRef(ref int b)
        {
            b 
= 2;
        }

        
static void funWithOut(out int c)
        {
            c 
= 3;//必须在函数体内对参数进行赋值
         }
    }
}

总结: 对于常规类别的,在作为函数的参数传递的时候,如果没有使用ref或out关键字,那么在函数体内部,对其进行的任何修改都不会影响其本身. 如果正确使用了ref或out关键字,那么对于参数的操作,其对应的引用的值也会改变.

二、  对象类型(object types)

ContractedBlock.gif ExpandedBlockStart.gif Example3
//Example3

using System;

namespace Program
{
    
class Program
    {
        
static void Main()
        {
            Test ts 
= new Test('a');

            Console.WriteLine(ts.C);
//输出a

            objFun(ts, 
'b');

            Console.WriteLine(ts.C);
//输出b

            objFunWithRef(
ref ts, 'c');

            Console.WriteLine(ts.C);
//输出c

            objFunWithOut(
out ts, 'd');

            Console.WriteLine(ts.C);
//输出d
        }
            
            
static void objFun(Test ts,char newC)
            {
                ts.C
=newC;
            }
            
            
static void objFunWithRef(ref Test ts,char newC)
            {
                ts.C
=newC;
            }

            
static void objFunWithOut(out Test ts,char newC)
            {
                ts
=new Test(newC);//记住要初始化
            }       
    }

    
class Test
    {
        
private char c;

        
public char C
        {
            
set { c = value; }
            
get { return c;  }
        }

        
//构造函数
        public Test(char c)
        {
            
this.c = c;
        }       
    }

}

 似乎我们从这个例子的输出中会发现,对象类型的对象作为函数的参数传递的时候,无论是否使用关键字ref和out,在函数体中作的任何修改都会修改到本身对象。而答案是这样的吗?首先我先告诉你并不是这样的,然后接下来再看下面的例子:

ContractedBlock.gif ExpandedBlockStart.gif Example4
//Example4

using System;

namespace Program
{
    
class Program
    {
        
static void Main()
        {
            Test ts 
= new Test('a');

            Console.WriteLine(ts.C);
//输出a

            objFun(ts, 
'b');

            Console.WriteLine(ts.C);
//输出a

            objFunWithRef(
ref ts, 'c');

            Console.WriteLine(ts.C);
//输出c

            objFunWithOut(
out ts, 'd');

            Console.WriteLine(ts.C);
//输出d
        }
            
            
static void objFun(Test ts,char newC)
            {
                ts
=new Test('a');
                ts.C
=newC;
            }
            
            
static void objFunWithRef(ref Test ts,char newC)
            {
                ts
=new Test('a');
                ts.C
=newC;
            }

            
static void objFunWithOut(out Test ts,char newC)
            {
                ts
=new Test(newC);//记住要初始化
              }           
    }

    
class Test
    {
        
private char c;

        
public char C
        {
            
set { c = value; }
            
get { return c;  }
        }

        
//构造函数
        public Test(char c)
        {
            
this.c = c;
        }        
    }
}


Example4与Example3的区别就是修改了objFun函数和objFunWithRef函数,但是输出的结果可能会让你感到非常的质疑,为解释这个问题,我们分两种情况讨论:
<1>   不使用ref/out关键字
对于这种情况,参数的传递情况和基本型别是一致的,pass by value.当我们写下
Test ts = new Test('a');
你可能并不知道,你已经在使用引用了.另一方面,你可能认为你创建了一个Test类的对象---ts.不过,很遗憾地告诉你,你错了.你的确创建了一个Test类的对象,但它不是tc,而是new Test('a')对应的一个在堆(Heap)上的对象.ts是与这个对象联系的引用,或说是指向那个对象的指针.

当参数传递的时候,正如我说的那样:pass by value.那么这里的value为何呢?是引用!当调用objFun(Test , char)时,如基本型别那样,产生一个引用的副本(copy).自然地,这个引用副本也是指向实参所指向的同一个对象: 

从这里我们可以看到,如果我们在函数体中修改了某个属性(像Example 3中那样),由于copy of ts与ts所指向的对象是同一的,修改自然也就会如实地反映到对象身上.这与我们在Example 3的输出中看到的情况是一致的.

如果像Example 4中那样使用了new关键字,根据我们现在的理解,应该是这样的:ts仍然是指向了原来的对象,而由于new的作用,ts的副本(copy of ts)指向了在Heap上创建的一个新对象如此一来,在函数体中对ts的副本的操作不会使原来的对象受到影响.对于我们的Example 4来说,调用了objFun后仍然输出了 a.

<2>   使用了ref/out关键字
对于使用了ref/out关键字修饰的对象类型实例,不会生成一个引用的副本,而是直接使用原来的引用(自然指向原来的对象).如此一来,任何操作都会如实地折射到原来的对象上.但上面的例子事实上并不清晰,我们将objFunWithRef(ref Test ,char )再次改写为:

static   void  objFunWithRef( ref  Test ts, char  newC)
{
     ts
= new  Test( ' x ' );  // 结果将输出 x
     
// ts.C=newC 如果这句不被注释掉,结果将仍输出 c
}


由于ref和new的作用,原来的对象将成为垃圾,在理论上最终会被GC回收.同时,ts与Heap上创建的另一个对象(这里就应该是new Test('x')的对象)建立关联.

三、结构类型(Struct type)
在了解结构类型参数传递的之前,我们必须明确一点结构数据类型属于值类型,看下面的示例


 

ContractedBlock.gif ExpandedBlockStart.gif Example5
//Example5
using System;

namespace Program
{
    
class Program
    {
        
static void Main()
        {
            TestStruct ts 
= new TestStruct('a');

            Console.WriteLine(ts.C);
//输出a

            structFun(ts, 
'b');

            Console.WriteLine(ts.C);
//输出a

            structFunWithRef(
ref ts, 'c');

            Console.WriteLine(ts.C);
//输出c

            structFunWithOut(
out ts, 'd');

            Console.WriteLine(ts.C);
//输出d    

            Console.ReadKey();
        }

        
static void structFun(TestStruct ts, char newC)
        {              
            ts.C
=newC;
        }

        
static void structFunWithRef(ref TestStruct ts, char newC)
        {               
            ts.C
=newC;
        }

        
static void structFunWithOut(out TestStruct ts,char newC)
        {
            ts
=new TestStruct(newC);//记住要初始化
        }  
    }

    
struct TestStruct
    {
        
private char c;

        
public char C
        {
            
set { c = value; }
            
get { return c;  }
        }

        
//构造函数
        public TestStruct(char c)
        {
            
this.c = c;
        }        
    }
}


事实上我们发现,structFun(TestStruct,char)的修改并没有影响到原来的值,这和常规类别的参数传递是类似的,而实际上也正是如此。

四、String类型(String type)
string的数据类型属于引用类型,但由于其特殊性,我们这里将他单独拿出来讨论。

ContractedBlock.gif ExpandedBlockStart.gif Example6
//Example6
using System;

namespace Program
{
    
class Program
    {
        
static void Main()
        {
            
string str1 = "aa";          

            stringFun(str1);

            Console.WriteLine(str1);
//输出aa

            
string str2 = "aa";//必须初始化

            stringFunWithRef(
ref str2);

            Console.WriteLine(str2);
//输出bb

            
string str3;//可以不用初始化

            stringFunWithOut(
out str3);

            Console.WriteLine(str3);
//输出bb   

            Console.ReadKey();
        }

        
static void stringFun(string s)
        {
            s 
= "bb";
        }

        
static void stringFunWithRef(ref string s)
        {
            s 
= "bb";
        }

        
static void stringFunWithOut(out string s)
        {
            s 
="bb";//记住要初始化
         }  
    }
}

通过上面的代码,我们发现string类型参数传递返回的结果和常规值类型参数传递(如:整型参数传递)返回的结果一致,这里把string类型单独拿出来,是为了区别对待string这种特殊类型。本质上string是引用类型,但是在实际计算和处理的时候,它处处透漏出值类型的特征。

所有关于可能会在实际问题中遇到的参数的情况都基本上已经讨论完毕,下面做个总结:
). 值类型

1a). 不使用ref/out关键字

    参数传递时,生成值的副本.对副本的操作不会影响到原来的值.

1b). 使用ref/out关键字

    直接使用原来的变量,对于所作的修改会在原来的变量上如实反映.

   2). 引用类型

2a). 不使用ref/out关键字

    参数传递时,生成引用的副本.若副本仍指向原来的对象,则修改会影响到原来的对象,否则不会.

2b). 使用ref/out关键字

    使用原来的引用,所有修改将如实反映到原来的对象上.


参考文章:
http://blog.sina.com.cn/s/blog_43fe27d40100c3kb.html
http://www.cnblogs.com/wjfluisfigo/archive/2009/05/04/1448101.html

 



 

转载于:https://www.cnblogs.com/Jason_z/archive/2009/10/13/1582659.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值