C#学习笔记 -- 方法的参数

发布时间 2023-05-21 01:12:49作者: Phonk

1、值参数

当你使用值参数, 通过将实参的值复制到形参的方式把数据传递给方法,方法被调用时, 系统执行如下操作

  • 在栈中为形参分配空间

  • 将实参的值复制给形参

class MyClass
{
    public int Val = 20;
}
class Program
{
    static void MyMethod(Myclass f1, int f2)
    {
        f1.Val = f1.Val + 5;
        f2 = f2 + 5;
        CW.(f1.Val, f2);
    }
    
    static void Main(string[] args)
    {
        MyClass a1 = new MyClass();
        int a2 = 10;
        MyMethod(a1, a2);
        CW.(a1.Val, a2);// 25 15 25 10
    }
}
  1. a1传入方法后, a1复制变成f1, 但f1引用仍然指向堆中的对象, 修改了a1.Val, 所以a1.Val f1.Val都是15

  2. a2传入方法后, 复制一份a2 -> f2给方法去执行, 此时a2, f2都在栈里, 方法里修改f2的值, 修改完f2, f2就, a2没修改, 所以a2还是为10, f2为15

2、引用参数

特点

  • 不会在栈上为形参分配内存

  • 形参的参数名将作为实参变量的别名, 指向相同的内存位置

使用注意

  • 使用引用参数时, 必须在方法的声明和调用中都使用ref修饰符

  • 实参必须是变量, 在用做实参前必须被赋值, 如果是引用类型变量, 可以赋值为一个引用或null

class MyClass
{
    public int Val = 20;
}
class Program
{
    static void MyMethod(ref Myclass f1, ref int f2)
    {
        f1.Val = f1.Val + 5;
        f2 = f2 + 5;
        CW.(f1.Val, f2);
    }
    
    static void Main(string[] args)
    {
        MyClass a1 = new MyClass();
        int a2 = 10;
        MyMethod(a1, a2);
        CW.(a1.Val, a2);// 25 15 25 15
    }
}
  • 使用ref关键字, 进入方法a1不复制, 直接给f1, f1指向堆中的对象,

  • 使用ref关键字, 进入方法a2不复制, f2就是a2, 在栈里是同一个元素, 所以直接是修改的a2

  • 方法执行之后, 形参的名称已经失效, 但是值类型a2的值和引用类型a1所指向的对象的值都被方法内的行为改变了

(1)引用类型作为值参数和引用参数

对于一个引用类型对象, 不管是将其作为值参数传递还是作为引用参数传递, 都可以在方法成员内部修改它的成员

A.将引用类型对象作为值参数传递
  • 如果在方法内创建一个新对象并赋值给形参, 将切断形参与实参之间的关联, 并且在方法调用结束后, 新对象也将不复存在

class MyClass
{
    public int Val = 20;
}
class Program
{
    static void RefAsParameter(MyClass f1)
    {
        f1.Val = 50;
        Console.WriteLine(f1.Val);
        f1 = new MyClass();
        Console.WriteLine(f1.Val);
    }
    
    static void Main(string[] args)
    {
        MyClass a1 = new MyClass();
        Console.WriteLine(a1.Val);
        RefAsParameter(a1);
        Console.WriteLine(a1.Val);
        //20 50 20 50 
    }
}
  1. 一开始new出来的a1.Val = 20,第一个Main方法中输出20

  2. 进入RefAsParameter方法,

    1. f1是a1的复制品, 指向同一个堆中的对象, 修改值为50, 此时修改f1就是修改a1第二个输出50

    2. 新new了一个对象, 这个对象和之前那个对象没关系, 所以这里的val还是类初始化的20, 第三个输出20

  3. 因为在方法中已经把堆中的a1.Val修改为50, 所以第四个输出50

B.将引用类型对象作为引用参数传递
  • 如果在方法内创建一个新对象并赋值给形参, 在方法结束后该对象依然存在, 并且是实参所引用的值

class MyClass
{
    public int Val = 20;
}
class Program
{
    static void RefAsParameter(ref MyClass f1)
    {
        f1.Val = 50;
        Console.WriteLine(f1.Val);
        f1 = new MyClass();
        Console.WriteLine(f1.Val);
    }
    
    static void Main(string[] args)
    {
        MyClass a1 = new MyClass();
        Console.WriteLine(a1.Val);
        RefAsParameter(ref a1);
        Console.WriteLine(a1.Val);
        //20 50 20 20 
    }
}
  • 在方法调用时, 形参和实参都指向堆中相同的对象

  • 对成员值修改会同时影响到形参和实参

  • 当方法创建新的对象并赋值给形参时, 形参和实参的引用都指向该新对象

  • 在方法结束后, 实参指向方法内创建的新对象

3、输出参数

输出参数用于方法体内把数据传出到调用代码, 他们的行为与引用参数类似. 要求如下

  • 必须在声明和调用中都使用out参数

  • 实参必须是变量, 不能是其他类型的表达式, 因为方法需要内存位置来保存返回值

void Method(out int val)
{
    ...
}
int y = 1;
Method(out y);

输出参数的形参充当实参的别名. 形参和实参都是同一块内存的名称, 在方法内堆形参做的任何改变在方法执行完成后, 都是可见的, 要求如下

  1. 因为方法内的代码在读取输出参数之前必须对其写入, 所以不可能使用输出参数把数据传入方法

    public void Add(out int outValue)
    {
        int var1 = outValue + 2;;//这样是错误的, 在方法赋值之前,无法读取输出变量
    }
  2. 方法内部, 给输出参数复制后才能读取他, 参数的初始值是无关的, 而且没有必要在方法调用之前为实参赋值

  3. 方法内部, 方法返回之前, 代码中每条可能的路径和都必须为所有输出参数赋值

  4. 对于输出参数, 形参就好像是实参的别名一样, 但是还有一个要求, 那就是它必须在方法内进行赋值

class MyClass
{
    public int Val = 20;
}
class Program
{
    static void MyMethod(out MyClass, out int f2)
    {
        f1 = new MyClass();
        f1.Val = 25;
        f2 = 15;
    }
    
    static void Main(string[] args)
    {
        MyClass a1 = null;
        int a2;
        Mymethod(out a1, out a2);
    }
}
  1. 在方法调用之前, 将要被用作实参的变量a1, a2已经在栈中

  2. 方法的开始, 形参的名称被设置为实参的别名, 可以认为变量a1和f1指向的是相同内存位置, a2和f2也是相同位置. a1和a2不在作用域之内, 所以不能在MyMethod中访问

  3. 方法执行之后, 形参的名称已经失效, 但是引用类型的a1和值类型的a2的值都被方法内的行为改变了

从C#7.0开始, 不再需要预先声明一个变量来用作out参数. 可以在调用方法时在参数列表中添加一个变量类型, 他将作为变量声明

static void Main()
{
    MyMethod(out MyClass a1, out int a2);
    a2 += 5;
    CW(a2); //20
}

4、参数数组

  • 作用: 允许多个特定类型的0个或多个实参对于一个特定的形参

  • 一个参数列表中, 只能有一个参数数组

  • 如果有, 他必须是列表的最后一个

  • 由参数数组表示的所有参数必须是同一类型

声明参数数组需要注意

  • 在数据类型前使用params修饰符

  • 在数据类型后放置一组空的方括号

  • 在如下示例中, 形参intVals可以代表0个或多个int实参

void ListInts(param int[] intVals)
{
    ...
}

(1)方法调用

为参数数组提供实参

  • 使用逗号分隔列表, 所有元素必须是声明方法头中的类型

    ListInts(1, 2, 3);
  • 使用一个该数据类型元素的一维数组

    int[] intArray = new int[]{1, 2, 3};
    ListInts(int Array);
  • 在调用时, 无需加params修饰符

延伸式

方法调用的第一种形式被称为延伸式, 这种形式在调用中是哟个独立的实参

void ListInts(params int[] intVals)
{
    ...
}
ListInts();
ListInts(1, 2, 3);
ListInts(4, 5, 6, 7);
ListInts(8, 9 ,10 ,11 ,12);

在使用一个为参数数组使用独立实参的调用时, 编译器将

  • 接受实参列表, 用实参列表在堆中创建并初始化一个数组

  • 数组的引用保存到栈中的形参里

  • 如果在对应形参数组的位置没有实参, 编译器会创建一个有0个元素的数组来使用

class MyClass
{
    public void ListInts(params int[] inVals)
    {
        if(inVals != null) && (inVals.length != 0)
        {
            for(int i = 0; i < intvals.length; i++)
            {
                intVals[i] *= 10;
                Console.WriteLine("${inVal[i]}");
                
            }
        }
    }
}
static void Main(string[] args)
{
    MyClass myClass = new MyClass();
    int first = 5;
    int second = 6;
    int third = 7;
    myclass.ListInts(first, second, third);
    Console.WriteLine("${first}, {second}, {third}");
    //50 60 70 
    //5, 6, 7
}

(2)将数组作为实参

static void Main(string[] args)
{
    MyClass myClass = new MyClass();
    int[] myArr = new int[]{5, 6, 7};
    myClass.ListInts(myArr);
    foreach(int var in myArr)
    {
        Console.WriteLine("${var}");
    }
    //50 60 70 
    //50 60 70
}

数组的元素在堆内被更改了

5、参数类型总结

参数类型 修饰符 是否在声明时使用 是否在调用时使用 执行
值参数 - - 系统把实参的值复制到形参
引用参数 ref 形参是实参的别名
输出参数 out 进包含一个返回的值, 形参是实参的别名
数组参数 params 允许传递可变数目的实参到方法

12、ref局部变量和ref返回

前面章节中ref关键字传递一个对象引用给方法调用, 在调用上下文中, 对象的任何改动方法返回后依旧可见.

ref返回功能则恰好相反, 他允许将一个引用发送到方法外, 然后在调用上下文内使用这个引用, 一个相关的功能是ref局部变量, 他允许一个变量是另一个变量的别名

(1)ref局部变量

  • 你可以使用它创建一个变量的别名, 即使引用的对象是值类型

  • 对任意一个变量的赋值都会反应到另一个变量上, 因为他们是引用同一个对象, 即使是值类型

  • 创建别名的语法需要使用关键字ref两次, 一次是在别名声明的类型前面, 另一次是在赋值运算符的右边, 被别名的变量前面

ref int y = ref x;
ref int 别名 = ref 被别名;
class Program
{
    static void Main(string[] args)
    {
        int x = 2;
        ref int y = ref x;
        Console.WriteLine($"x = {x}, y = {y}");
        x = 5;
        Console.WriteLine($"x = {x}, y = {y}");
        y = 6;
        Console.WriteLine($"x = {x}, y = {y}");
        //2 2  5 5  6 6
    }
}

(2)ref返回

ref最常与ref返回功能一起使用. ref返回功能提供了一种使方法返回变量引用而不是变量值的方法, 这里需要的额外语法也使用了ref关键字两次

ref int var = ref Method(ref param);
  • 一次是在方法的返回类型声明之前

  • 另一次在return关键字之后, 被返回对象的变量名之前

class Simple
{
    private int Score = 5;
    
    public ref int RefToValue()
    {
        return ref Score;
    }
    
    public void Display()
    {
        Console.WriteLine($"{Score}");
    }
}
class Program
{
    static void Main(string[] args)
    {
        Simple s = new Simple();
        s.Display();//5
        //此时返回的是成员在内存上的引用地址, 而不单纯是值
        ref int v1OutSide = ref s.RefToValue();
        //在调用域外面修改值, 直接修改了成员的值
        v1OutSide = 10;
        s.Dsiplay();//5
    }
}

Math库中的Max方法的变形, 提供两个数字类型的变量, Math.Max能够返回两个值中比较大的那个, 但是假设你想返回的是包含较大值的变量的引用, 可以用ref返回

class Program619
{
    public static ref int Max(ref int p1, ref int p2)
    {
        if (p1 > p2)
        {
            return ref p1;
        }
        else
        {
            return ref p2;
        }
    }
}
    class Program619Test
    {
        static void Main(string[] args)
        {
            int v1 = 10;
            int v2 = 20;
            Console.WriteLine("start");
            Console.WriteLine($"v1 = {v1}, v2 = {v2}");// 10, 20
​
            ref int max = ref Program619.Max(ref v1, ref v2);
            Console.WriteLine("after method");
            Console.WriteLine($"max = {max}");//20
​
            max++;
            Console.WriteLine("after increment");
            Console.WriteLine($"v1 = {v1}, v2 = {v2}");// 10, 21
        }
    }

(3)ref的限制

  • 不能将void方法声明为ref返回方法

  • ref return 不能返回如下内容

    • 空值

    • 常量

    • 枚举成员

    • 类或结构体属性

    • 指向只读位置的指针

  • ref return 只能指向原先在调用域内的位置, 或者字段, 所以它不能指向方法的局部变量

  • ref局部变量只能被赋值一次, 一旦初始化, 他就不能指向不同的存储位置了

  • 即使将一个方法声明为ref返回方法, 如果在调用该方法时省略了ref关键字, 则返回的将是值, 而不是指向值的内存位置的指针

  • 如果将ref局部变量作为常规的实际参数传递给其他方法, 则该方法仅获取该变量的一个副本. 尽管ref局部变量包含指向存储位置的指针, 但是当以这种方式使用时, 他会传递值而不是引用