C#:传值参数、引用参数、输出参数、数组参数、具名参数、可选参数、扩展方法

184 阅读7分钟

  传值参数(值参数)

1、数据类型为值类型:

        值参数也就是未用ref和out修饰符修饰(未使用任何修饰符)声明的参数,在值参数被方法调用时会在栈中被分配内存以形参的方式来被调用;也就是说在堆中的实参不会参与方法的调用,而是在栈中分配新的内存将实参传入,以栈中形参(实参在栈中的副本)的方式被方法调用。

class Program
    {
        static void Main(string[] args)
        {
            Student Jia1 = new Student();
            int y = 100;
            Jia1.Addone(y); //将y的形参传入方法进行调用
            Console.WriteLine(y);
            Console.ReadKey();
        }

    }
    class Student //声明一个“类”
    {
        public void Addone(int x) //这里传进方法的是一个值类型的参数
        {
            x = x + 1;
            Console.WriteLine(x);
        }

    }

​编辑

这里输出x=101,y=100,所以说明了”Addone“方法只对y的形参(也就是在栈中的副本)进行操作,而不会对在堆中的y有任何影响。 

2、数据类型为引用类型,并且调用时使用“new”操作符创建了新的对象交给”形参“去引用:

         与值类型一样,方法都是对所谓的”形参“进行操作,而实参不会受到影响;在方法调用前后(赋值前),引用变量和引用参数的初始值(实参和形参)都指向的是同一个对象(存储着同一对象的地址);在赋值之后(方法内部),由于传进来的(方法调用的)是一个引用类型的值,那么这个引用类型的”形参“被赋值之后会由于赋值号的右边为new操作符的表达式,进而又创建了一个新的对象并且调用对象的实例构造器然后把传进来的值赋给这个对象,而新的引用参数则存储的是新的对象的地址。

简单的来说就是这个所谓的副本,就是一个新的引用类型的变量,这个副本所指向的对象是新的对象(副本的对象),新的对象拥有新的地址

class Program
    {
        static void Main(string[] args)
        {
            Student Jia = new Student() { Name="Alex"};
            SomeMethod(Jia);
            Console.WriteLine("Hashcode={0}, Name={1},",Jia.GetHashCode(), Jia.Name);
            Console.ReadKey();
        }
        static void SomeMethod(Student name)
        {
            Student stu = new Student() { Name="Tony"};
            Console.WriteLine("Hashcode={0}, Name={1},", stu.GetHashCode(), stu.Name);

        }

    }
    class Student //声明一个“类”
    {
        public string Name { get; set; } //为这个类声明一个简单的属性
    }

​编辑

        这里可看到托尼和阿利克斯两个值的地址是不同的,所以在方法内部把阿利克斯的名字换成托尼的时候是创建新的对象”stu“,这个”stu“所储存的地址是一个新的地址,但是由于被赋了新的值(也就是托尼),那么阿列克斯就变成了托尼。还有一个值得注意的就是,如果不对新的对象进行赋值或者依旧是赋 ”阿列克斯“,那么得到的结果会是两个阿列克斯,但是对象的地址两者会完全不同。这样就印证了下图的说法。

(其实这种方法在工作当中使用的并不多,第二种情况是偏学院派一些的)

​编辑

3、数据类型为引用类型,但只操作形参所引用的对象,不使用new操作符对其再进行实例化:

        同样是数据类型为引用类型,但是在方法体中不会创建新的对象,而是直接引用实参所引用的对象,所以在进行赋值之后,由于引用的是同一对象的地址,所以也就对那唯一被引用的对象进行修改,也就是对其进行了更新。

​编辑

 从图解上看,方法体外和内操作的其实同一个对象。

class Program
    {
        static void Main(string[] args)
        {
            Student Jia = new Student() { Name="Alex"};
            SomeMethod(Jia);
            Console.WriteLine("Hashcode={0}, Name={1},",Jia.GetHashCode(), Jia.Name);
            Console.ReadKey();
        }
        static void SomeMethod(Student stu)
        {
            stu.Name = "Tony";//在方法内对同一对象“Name”进行修改
            Console.WriteLine("Hashcode={0}, Name={1},", stu.GetHashCode(), stu.Name);

        }

    }
    class Student //声明一个“类”
    {
        public string Name { get; set; } //为这个类声明一个简单的属性
    }

​编辑

        从哈希值上也能看得出,方法体内和方法体外,其实指向的是同一对象的地址。 这种直接对对象以值参数的形式在方法中进行引用然后修改的方法叫做某个方法的”副作用(side-effect)“,在工作当中这种方法使用的非常的少,我们调用方法的时候大多数是为了得到调用方法后输出的返回值;当然也有通过此”副作用“来显式修改对象参数的使用情况。

=========================================================================

引用参数

        引用参数是用”ref“修饰符来声明的”形参“。与值参数的形参不同,引用形参并不创建新的存储位置,没有new操作符来创建新的对象。。。引用形参表示的存储位置其实就是对应的实参存储数据的位置。

ps:在变量被用作引用参数进行传递的时候,必须先明确赋值,否则此引用将毫无意义

1、数据类型为值类型:

        由于形参与实参指向的是同一个存储数据的地址,值类型以引用参数的方式传进方法的时候,它在方法体中就变成了一个形参,在方法体内对应用进来的实参,也就是对形参进行赋值的时候,由于引用参数的形参实参指向的是内存当中的同一个地址,所以会同时改变方法内部的形参和方法外部的实参。

       

​编辑

static void Main(string[] args)
        {
            
            int y = 100;  //方法体外声明值类型变量
            SomeMethod(ref y);  //将值类型以引用参数形式传进方法体内
            Console.WriteLine(y);
            Console.ReadKey();
        }

        static void SomeMethod(ref int x)  //将值类型以引用参数形式传进方法体内
        {  
            x = x + 1;
        }

在方法体外输出值  y=101;

很明显,带有引用参数的方法其实也是显式地改变形参对应的实参的值,这其实也是一种”副作用“,与上面讲的”值参数的第三种使用方法“很相似。

2、数据类型为引用类型,并且调用时使用“new”操作符创建了新的对象交给”形参“去引用:

        引用类型的形参和实参在这里其实存储的是同一对象的地址,在方法体内部创建新对象的时候就已经把新的对象的地址赋给形参了, 然后再把”Tony“赋给新对象的属性name,那么形参就有了新的对象地址和新的对象属性值。但是由于型参与实参指向的是同一个地址,所以在方法体内部对形参进行修改也就是对方法体外部的实参进行修改了。

class Program
    {
    static void Main(string[] args)
        {
            Student 实参 = new Student() { Name = "Alex" };
            //创建一个对象,名为“实参”,并对对象属性name赋值Alex
            
            Console.WriteLine("Hashcode={0}, Name={1},", 实参.GetHashCode(), 实参.Name);
            //此时实参的地址和对象属性的值均未改变
            
            SomeMethod(ref 实参);
            //将实参以引用参数传进方法体内部
            
            Console.WriteLine("Hashcode={0}, Name={1},", 实参.GetHashCode(), 实参.Name);
            //由于引用参数的形参实参指向的是同一地址,所以当形参被创建新的对象并且赋新值的时候,方        法外部的实参会随着方法内部的形参一起改变,变成了新对象的地址和新对象的值
            Console.ReadKey();
        }

        
        static void SomeMethod(ref Student 形参)
        {
            形参 = new Student() { Name="Tony"};
            //此时的实参已经在方法体内部变成了形参并且在方法体内部创建了新的对象给形参赋予了新值
            
            Console.WriteLine("Hashcode={0}, Name={1},", 形参.GetHashCode(), 形参.Name);
            //此时将形参输出,哈希值和对象name的值均改变
        }
    }
class Student //声明一个“类”
    {
        public string Name { get; set; } //为这个类声明一个简单的属性
    }

​编辑

        在这里就能看出,形参被修改时,实参也跟着被修改了,但注意这里是对同一个对象地址进行修改,而且从头到尾只对一个对象地址进行操作。

3、数据类型为引用类型,但只操作形参所引用的对象,不使用new操作符对其再进行实例化:

        这种方式其实是只通过这个引用参数(只通过形参),对对象的地址进行访问并就修改对象的值,和值参数直接修改变量值其实是差不多的,只是内存机理上不一样。值参数的方式是直接改变对象(内部字段或属性)的值,而引用参数是先通过访问对象的地址,然后再对对象(内部字段或属性)的值进行修改,前者的传递数据是值,后者传递的数据是地址。

​编辑

class Program
    {
        static void Main(string[] args)
        {
            Student 实参 = new Student() { Name = "Alex" };
            //创建一个对象,名为“实参”,并对对象属性name赋值Alex
            Console.WriteLine("Hashcode={0}, Name={1},", 实参.GetHashCode(), 实参.Name);
            //此时实参的地址和对象属性的值均未改变
            SomeMethod(ref 实参);
            //将实参以引用参数传进方法体内部
            Console.WriteLine("Hashcode={0}, Name={1},", 实参.GetHashCode(), 实参.Name);
            //由于引用参数的形参实参指向的是同一地址,所以当形参被创建新的对象并且赋新值的时候,方法外部的实参会随着方法内部的形参一起改变
            Console.ReadKey();
        }

        
        static void SomeMethod(ref Student 形参)
        {                       
            形参.Name = "Tony";
            //在方法内对同一对象“Name”进行修改
            
            Console.WriteLine("Hashcode={0}, Name={1},", 形参.GetHashCode(), 形参.Name);
            //此时将形参输出,哈希值和对象name的值均改变
        }

    }
class Student //声明一个“类”
    {
        public string Name { get; set; } //为这个类声明一个简单的属性
    }

​编辑

在这里就能看出,形参被修改时,实参也跟着被修改了,但注意这里形参和实参存储的是同一个地址,而这个地址就是在堆内存中对象的地址,是对同一个地址的对象进行修改,而且从头到尾只对一个对象进行操作。