方法的构成

方法主要有两部分:方法头和方法体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//返回类型,方法名称,参数列表
int MyMethod(int par1, string par2);

static void Main()
{
//局部变量,初始化为3
int MyInt = 3;
//控制流结构
while(myInt > 0)
{
--myInt;
//方法调用
printMyMessage();
}
}

局部变量

对比维度 实例字段 局部变量
生存期 从实例被创建时开始,直到实例不再被访问时结束 从它在块中被声明的那一刻开始,在块完成执行时结束
隐式初始化 初始化成该类型的默认值 没有隐式初始化。如果变量在使用之前没有被赋值,编译器就会产生一条错误消息
存储区域 由于实例字段是类的成员,所以所有字段都存储在堆里,无论它们是值类型的还是引用类型的 值类型:存储在栈里

引用类型:引用存储在栈里,数据存储在堆里

类型推断与var关键字

var关键字并不是表示特殊变量。只是语法上的速记,表示任何可以从初始化语句的右边推断出的类型

  • 只能用于局部变量,不能用于字段
  • 只能在变量声明中包含初始化时使用
  • 一旦编译器推断出变量的类型,它就是固定且不能更改的。

嵌套块中的局部变量

局部变量可以在嵌套块的内部声明,并且和所有的局部变量一样,它们的生存期和可见性仅限于声明它们的块及内嵌块。
在c#中不管嵌套级别如何,都不能在第一个名称的有效范围内声明另一个同名的局部变量。

1
2
3
4
5
6
7
8
9
void Method1
{
int var1 = 5;
{
int var2 = 10;
//...
}
//...
}

局部常量

  • 在声明式必须初始化。
  • 在声明后不能改变。
1
2
3
4
void Display()
{
const double PI = 3.1416;
}

控制流

参考之前的文章C#控制流语句(六)

[[csharp-6-control]]

方法的调用

可以从方法体的内部调用其他方法。
调用其他方法要使用方法名并带上参数列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyClass 
{
void PrintDateAndTime()
{
DateTime dt = DateTime.Now;
Console.WriteLine($"{ dt }");
}

static void Main()
{
MyClass mc = new MyClass();
mc.PrintDateAndTime(); //调用方法
}
}

返回值

1
2
3
4
5
6
7
8
9
10
11
//int: 返回类型
int GetHour()
{
return hour; //返回一个int
}

MyClass method3()
{
MyClass mc = new MyClass();
return mc;
}

返回语句与void方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyClass 
{
//void返回类型
void TimeUpdate()
{
DateTime dt = DateTime.Now;
if (dt.Hour < 12)
return; //返回调用方法

Console.WriteLine("It's afternoon!");
}

static void Main()
{
MyClass mc = new MyClass();
mc.TimeUpdate();
}
}

局部函数

从c#7.0开始,可以在一个方法中声明另一个单独的方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Program
{
public void MethodWithLocalFunction()
{
int MyLocalFunction(int z1) //声明局部函数
{
return z1 * 5;
}
}

int results = MyLocalFunction(5); //调用局部函数
Console.WriteLine($"Results of local function call: {results}");

static void Main(string[] args)
{
Program myProgram = new Program();
myProgram.MethodWithLocalFunction(); //调用方法
}
}

参数

形参

1
2
3
4
5
public void PrintSum(int x, int y)
{
int sum = x + y;
Console.WriteLine($"Newsflash: { x } + { y } is { sum }");
}

实参

1
PrintSum(5, someInt);

值参数与引用参数

值参数

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

  • 在栈中为形参分配空间。
  • 将实参的值复制给形参。
1
2
3
4
5
6
7
8
float func1(float val_)    //声明方法
{
float j = 2.6F;
float k = 5.1F;
//...
}
float fValue1 = func1( k ); //方法调用
float fValue2 = func1( (k + j) / 3 ); //方法调用

引用参数

  • 使用引用参数时,必须在方法的声明和调用中都使用ref修饰符。
  • 实参必须是变量,在用作实参前必须被赋值。如果是引用类型变量,可以赋值为一个引用或null。
  • 不会在栈上为形参分配内存。
  • 形参的参数名将作为实参变量的别名,指向相同的内存位置。所以在方法的执行过程中对形参的改变在方法完成后依然可见。
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
class MyClass
{
public int Val = 20; // 初始化字段为 20
}

class Program
{
static void MyMethod(ref MyClass f1, ref int f2)
{
f1.Val = f1.Val + 5; // 参数的字段加 5
f2 = f2 + 5; // 另一参数加 5
Console.WriteLine($"f1.Val: {f1.Val }, f2: {f2}");
}

static void Main()
{
MyClass a1 = new MyClass();
int a2 = 10;

MyMethod(ref a1, ref a2); // 调用方法
Console.WriteLine($"a1.Val: {a1.Val }, a2: {a2}");
}
}
/*
* f1.Val: 25, f2: 15
* a1.Val: 25, a2: 15
*/

注意:不管MyClass对象f1是否通过ref传递给方法,f1.Val的值都是相同的。

引用类型作为值参数和引用参数

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

将引用类型对象作为值参数传递:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyClass { public int Val = 20; }

class Program
{
static void RefAsParameter( MyClass f1 )
{
f1.Val = 50;
Console.WriteLine($"After member assignment: { f1.Val }");
f1 = new MyClass();
Console.WriteLine($"After new object creation: { f1.Val }");
}

static void Main( )
{
MyClass a1 = new MyClass();
Console.WriteLine($"Before method call: { a1.Val }");
RefAsParameter( a1 );
Console.WriteLine($"After method call: { a1.Val }");
}
}
1
2
3
4
Before method call:    20
After member assignment: 50
After new object creation: 20
After method call: 50

将引用类型对象作为引用参数传递:

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
class MyClass
{
public int Val = 20;
}

class Program
{
static void RefAsParameter( ref MyClass f1 )
{
//设置对象成员
f1.Val = 50;
Console.WriteLine($"After member assignment: { f1.Val }");

//创建新对象并赋值给形参
f1 = new MyClass();
Console.WriteLine($"After new object creation: { f1.Val }");
}

static void Main( string[] args )
{
MyClass a1 = new MyClass();

Console.WriteLine($"Before method call: { a1.Val }");
RefAsParameter( ref a1 );
Console.WriteLine($"After method call: { a1.Val }");
}
}
1
2
3
4
Before metho4d call:    20
After member assignment: 50
After new object creation: 20
After method call: 20

输出参数

输出参数用于从方法体内把数据传出到调用代码,它们的行为与引用参数类似。

  • 必须在声明和调用中都使用修饰符。输出参数的修饰符是out
  • 和引用参数相似,实参必须是变量。
  • 与引用参数类似,输出参数的形参充当实参的别名,形参和实参都是同一块内存位置的名称,在方法内对形参的任何改动在方法执行完成后都是可见的。
    与引用参数的不同:
  • 在方法内部,给输出参数赋值之后才能读取它。意味着与参数的初始值是无关的,而且没必要在方法调用之前为实参赋值。
  • 在方法内部,在方法返回之前,代码中每条可能的路径都必须给所有输出参数赋值。
    因为方法内部的代码在读取输出参数之前必须对其写入,所以不能使用输出参数把数据传入方法。如果方法中有任何执行路径试图在方法给输出参数赋值之前读取它的值,编译器就会产出一条错误消息。
1
2
3
4
public void Add2(out int outValue)
{
int var1 = outValue + 2; //出错了!在方法赋值之前无法读取输出变量
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MyClass
{
public int Val = 20; // 字段初始化为 20
}

class Program
{
static void MyMethod(out MyClass f1, out int f2)
{
f1 = new MyClass(); // 创建一个类变量
f1.Val = 25; // 赋值类字段
f2 = 15; // 赋值 int 参数
}

static void Main()
{
MyClass a1 = null;
int a2;

MyMethod(out a1, out a2); // 调用方法
}
}

参数数组

方法调用

之前的参数类型中,一个形参必须严格对应一个实参,而参数数组则不同,它允许特定类型的零个或多个实参对应一个特定的形参。

  • 一个参数列表中只能有一个参数数组。
  • 如果有参数数组,必须是列表中的最后一个。
  • 在数据类型前使用params修饰符。
1
2
3
4
5
6
7
8
void ListInts(params int[] inVals)
{
//...
}

ListInts(10, 20, 30);
int[] intArray = {1, 2, 3};
ListInts( intArray );
  • 值参数的声明和调用都不带修饰符。
  • 引用参数和输出参数在声明和调用都需要修饰符。
  • 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
class MyClass
{
public void ListInts( params int[] inVals )
{
if ( (inVals != null) && (inVals.Length != 0) )
{
for (int i = 0; i < inVals.Length; i++) // 处理数组
{
inVals[i] = inVals[i] * 10;
Console.WriteLine($"{ inVals[i] }"); // 显示新值
}
}
}
}

class Program
{
static void Main()
{
int first = 5, second = 6, third = 7; // 声明 3 个 int

MyClass mc = new MyClass();
mc.ListInts( first, second, third ); // 调用方法

Console.WriteLine($"{ first }, { second }, { third }");
}
}
1
2
3
4
50
60
70
5, 6, 7

将数组作为实参

也可以在方法调用之前创建并组装一个数组,把单一的数组变量作为实参传递,这种情况下,编译器使用你的数组而不是重新创建一个。

1
2
3
4
5
6
7
static void Main() {
int[] myArr = new int[] { 5, 6, 7 }; // 创建并初始化数组
MyClass mc = new MyClass();
mc.ListInts(myArr);
foreach (int x in myArr)
Console.WriteLine($" { x }"); // 输出每个元素
}
1
2
3
4
5
6
50
60
70
50
60
70

参数类型总结

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

ref局部变量和ref返回

ref局部变量,允许一个变量是另一个变量的别名。

  • 可以使用这个功能创建一个变量的别名,即使引用的对象是值类型。
  • 对任意一个变量的赋值都会反应到另一个变量上,因为它们引用的是相同的对象,即使是值类型。
    创建别名的语法需要使用关键字ref两次,一次在别名声明的类型的前面,一次是赋值运算符的右边。ref int y = ref x;

ref局部变量实际上经常和ref返回功能一起使用。ref返回功能提供了一种使方法返回变量引用而不是变量值的方法。
ref返回功能也是使用关键字ref两次:

  • 一次是在方法的返回类型声明之前
  • 另一次是return关键字之后,被返回对象的变量名之前
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
class Simple
{
private int Score = 5;

public ref int RefToValue()
{
return ref Score;
}

public void Display()
{
Console.WriteLine($"Value inside class object: {Score}");
}
}

class Program
{
static void Main()
{
Simple s = new Simple();
s.Display();

ref int v1Outside = ref s.RefToValue();
v1Outside = 10; // 在调用域外修改值
s.Display(); // 检查值是否已经改变
}
}
1
2
Value inside class object: 5
Value inside class object: 10
  • 不能将返回类型是void的方法声明为ref返回方法。
  • ref return表达式不能返回如下内容:
    • 空值
    • 常量
    • 枚举成员
    • 类或者结构体的属性
    • 指向只读位置的指针
  • ref return表达式只能指向原先就在调用域内的位置,或者字段。所以,它不能指向方法的局部变量
  • ref局部变量只能被赋值一次,也就是说,一旦初始化,它就不能指向不同的存储位置了。
  • 即使将一个方法声明为ref返回方法,如果在调用该方法时省略了ref关键字,则返回的将是值,而不是指向值的内存位置的指针。
  • 如果将ref局部变量作为常规的实际参数传递给其他方法,则该方法仅获取该变量的一个副本。尽管ref局部变量包含指向存储位置的指针,但是当以这种方式使用时,它会传递值而不是引用。

方法重载

方法重载就是指一个类中多个同名方法。
方法的签名有一下部分:

  • 方法的名称
  • 参数的数目
  • 参数的数据类型和顺序
  • 参数修饰符
1
2
3
4
5
6
7
class A
{
long AddValues(int a, int b) { return a + b; }
long AddValues(int c, int d, int e) { return c + d + e; }
long AddValues(float f, float g) { return (long)(f + g); }
long AddValues(long h, long m) { return h + m; }
}

返回类型,形参名称不是签名的一部分

1
2
3
4
5
class B  //错误的示例
{
long AddValues(long a, long b) { return a + b; }
int AddValues(long c, long d) { return c + d; } // 错误,相同的签名
}

命名参数

c#还允许使用命名参数(named parameter)。只要显示指定参数的名字,就可以任意顺序在方法调用中列出实参。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyClass {
public int Calc( int a, int b, int c )
{ return ( a + b ) * c; }
}

static void Main() {
MyClass mc = new MyClass( );

int r0 = mc.Calc( 4, 3, 2 ); // 位置参数
int r1 = mc.Calc( 4, b: 3, c: 2 ); // 位置参数和命名参数
int r2 = mc.Calc( 4, c: 2, b: 3 ); // 交换了顺序
int r3 = mc.Calc( c: 2, b: 3, a: 4 ); // 所有都是命名参数
int r4 = mc.Calc( c: 2, b: 1 + 2, a: 3 + 1 ); // 命名参数表达式

Console.WriteLine($" { r0 }, { r1 }, { r2 }, { r3 }, { r4 }");
}

命名参数有助于显示哪个值赋给哪个形参。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyClass {
double GetCylinderVolume( double radius, double height )
{
return 3.1416 * radius * radius * height;
}

static void Main( string[] args ) {
MyClass mc = new MyClass();
double volume;

volume = mc.GetCylinderVolume( 3.0, 4.0 );
// ...
volume = mc.GetCylinderVolume( radius: 3.0, height: 4.0 );
// ...
}
// 更多信息
}

可选参数

可选参数就是在调用方法的时候包含这个参数,也可以省略它。

参数类型 ref out params
值类型
引用类型 只允许null的默认值
  • 只要是值类型的默认值在编译时可以确定,就可以使用值类型作为可选参数。
  • 只有在默认值是null的时候,引用类型才可以用作可选参数。
  • 所有必填参数(required paramenter)必须在可选参数声明之前声明,params参数(参数数组),必须在所有可选参数之后声明。
    • (int x, decimal y, ... int op1 = 17, double op2 = 36, ... params int[] intVals)
  • 必须从可选参数列表的最后开始省略,不能随意选择省略任意的可选参数,要从最后开始。
  • 如果需要随意省略可选参数列表中的可选参数,需要结合利用命名参数和可选参数特性。
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
class MyClass
{
double GetCylinderVolume( double radius = 3.0, double height = 4.0 )
{
return 3.1416 * radius * radius * height;
}

static void Main( )
{
MyClass mc = new MyClass();
double volume;

volume = mc.GetCylinderVolume( 3.0, 4.0 ); //位置参数
Console.WriteLine( "Volume = " + volume );

volume = mc.GetCylinderVolume( radius: 2.0 ); //使用height默认值
Console.WriteLine( "Volume = " + volume );

volume = mc.GetCylinderVolume( height: 2.0 ); //使用radius默认值
Console.WriteLine( "Volume = " + volume );

volume = mc.GetCylinderVolume( );
Console.WriteLine( "Volume = " + volume ); //使用两个默认值

volume = mc.GetCylinderVolume( );
Console.WriteLine( "Volume = " + volume );
}
}

栈帧

在调用方法时,内存从栈的顶部开始分配,保持和方法关联的一些数据项,这块内存叫做方法的栈帧(stack frame)。

  • 栈帧包含的内存保存如下内容:
    • 返回地址,也就是方法退出的时候继续执行的位置。
    • 分配内存的参数,也就是方法的值参数,还可能是参数数组。
  • 在方法调用时,整个栈帧都会压入栈。
  • 在方法退出时,整个栈帧都会从栈上弹出。弹出栈帧也叫做栈展开(unwind)。

递归

递归:方法调用自身。
调用方法自身的机制和调用其他方法完全一样,都是为每一次方法调用把新的栈帧压入栈顶。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Program
{
public void Count(int inVal)
{
if (inVal == 0)
return;
Count(inVal - 1);
//再一次调用方法
//调用自身
Console.WriteLine($" {inVal }");
}

static void Main()
{
Program pr = new Program();
pr.Count(3);
}
}
1
2
3
1
2
3