第6章 方法 笔记
6.1 方法的结构
方法是一段具有名称的代码。
方法主要有两部分:方法头、方法体。
方法头指定方法的特征,包括:
- 返回类型。
- 方法名称。
- 参数列表。
方法体 包含可执行代码的语句序列。
6.2 方法体内部的代码执行
方法体是一个块,包含以下内容:
- 局部变量;
- 控制流结构;
- 方法调用;
- 内嵌的块;
- 其他方法(局部函数)。
static void Main()
{
// 局部变量
int myInt = 3;
// 控制流
while(myInt > 0)
{
--myInt;
// 调用方法
PrintMyMessage();
}
}
6.3 局部变量
局部变量用于保存局部或者临时的计算数据。
- 局部变量的生存周期仅限于创建它的块内。
- 声明时开始存在。
- 块尾结束存在。
- 可以在方法体内部任意位置声明,必须声明后才能使用。
6.3.1 类型推断和 var 关键字
var关键字是句法上的速记,可以从初始化语句的右边推断出类型。使用 var 关键字不需要明确指定变量类型。var 关键字并不改变 C# 的强类型性质。
使用条件:
- 只能用于局部变量,不能用于字段。
- 只能在变量声明中包含初始化时使用。
- 一旦编译器推断出变量的类型,它就是固定且不能更改的。
static void Main()
{
// 显示声明
int total = 15;
// 使用var
var total = 15;
}
6.3.2 嵌套块中的局部变量
方法体内部可以嵌套其他的块。
在 C# 中不论嵌套级别如何,都不能在第一个名称的有效范围内,声明另一个同名的局部变量。
6.4 局部常量
局部常量必须声明在块的内部。
重要特征
- 声明时必须初始化。
- 初始化值必须在编译时就可以确定,通常为预定义简单类型或 null 引用。
- 声明后不能改变。
// 声明语法示例:
const Type Identifier = Value;
// 举例:
const double PI = 3.14;
注意:const 不是修饰符,而是核心声明的一部分,必须放在类型前面。
和局部变量一样,局部常量声明在方法体或代码块里,并在声明它的块结束的地方失效。
6.5 控制流
控制流语句运行改变执行的顺序
常用的控制流语句:
- 选择语句 可以选择要执行的语句或语句块
- if
- if … else
- switch
- 迭代语句 可以在一个语句块上循环
- for循环 - 顶部
- while循环 - 顶部
- do循环 - 底部
- foreach
- 跳转语句 可以从代码块或方法体内部一个地方跳到另一个地方
- break 跳出当前循环
- continue 到当前循环底部
- goto 跳转到一个命名的语句
- return 返回到调用方法继续执行
void SomeMethod() {
int intVal = 3;
// if statement
if( intVal == 3 )
Console.WriteLine("Value is 3. ");
// for statement
for( int i=0; i<5; i++ )
Console.WriteLine($"Value of i: { i }");
}
6.6 方法调用
可以在方法体的内部调用其他方法,调用时需要使用方法名,并加上参数列表。
class MyClass
{
// Declare the method.
void PrintDateAndTime()
{
// Get the current date and time.
DateTime dt = DateTime.Now;
// Write it out.
Console.WriteLine($"{ dt }");
}
// Declare the method.
static void Main()
{
MyClass mc = new MyClass();
// Invoke the method.
mc.PrintDateAndTime( );
}
}
6.7 返回值
方法可以返回一个值,
要返回值,需要在方法名前面声明一个返回类型,使用返回语句从方法中返回一个值。
int GetHour( )
{
// Get the current date and time.
DateTime dt = DateTime.Now;
// Return an int.
int hour = dt.Hour;
// Return an int.
return hour;
}
6.8 返回语句和 void 方法
使用返回语句提前退出方法,只能用于 void 方法
class MyClass
{
void TimeUpdate()
{
DateTime dt = DateTime.Now; // Get the current date and time.
if (dt.Hour < 12) // If the hour is less than 12,
return; // then return.
Return to calling method
Console.WriteLine("It's afternoon!"); // Otherwise, print message.
}
static void Main()
{
MyClass mc = new MyClass(); // Create an instance of the class.
mc.TimeUpdate(); // Invoke the method.
}
}
6.9 局部函数
C# 7.0 开始,可以在一个方法中声明另一个单独的方法,称为局部函数。
class Program
{
public void MethodWithLocalFunction()
{
// Local function declaration
int MyLocalFunction(int z1)
{
return z1 * 5;
}
// Call local function
int results = MyLocalFunction(5);
Console.WriteLine($ "Results of local function call: {results}");
}
static void Main(string[] args)
{
Program myProgram = new Program();
myProgram.MethodWithLocalFunction(); // Call Method
}
}
6.10 参数
参数用来传入数据、返回数据
形参是局部变量,声明在方法的参数列表中。
形参有类型、名称,声明必须用逗号隔开,参数在方法体外面定义并初始化(输出参数是例外)
public void PrintSum( int x, int y )
{
int sum = x + y;
Console.WriteLine($"Newsflash: { x } + { y } is { sum }");
}
实参用于初始化形参的表达式或变量,
实参位于方法调用的参数列表中,
每一个实参必须与对应形参的类型相同,或者编译器能够把实参隐式转换为对应类型
class MyClass
{
// Formal parameters
public int Sum(int x, int y)
{
return x + y;
}
// Formal parameters
public float Avg(float input1, float input2)
{
return (input1 + input2) / 2.0 F;
}
}
class Program
{
static void Main()
{
MyClass myT = new MyClass();
int someInt = 6;
Console.WriteLine("Newsflash: Sum: {0} and {1} is {2}",
5, someInt, myT.Sum(5, someInt));
Console.WriteLine("Newsflash: Avg: {0} and {1} is {2}",
5, someInt, myT.Avg(5, someInt));
}
}
6.11 值参数
值参数,通过将实参的值复制到形参把数据传到方法。
使用值参数时,会发生如下操作:
在栈中为形参分配空间。
将实参的值复制给形参。
值参数的实参可以是变量(需要被赋值)、表达式。
方法使用值参数不能改变原始的值类型数据,但是可以改变引用类型的数据:
class MyClass {
public int Val = 20;
}
class Program
{
// 形参
static void MyMethod(MyClass f1, int f2)
{
f1.Val = f1.Val + 5;
f2 = f2 + 5;
}
static void Main()
{
MyClass a1 = new MyClass();
int a2 = 10;
// 实参
MyMethod(a1, a2);
}
}
6.12 引用参数
引用参数是第二种参数类型
在使用引用参数时,必须在方法的声明和调用中都使用 ref 修饰符。
实参必须是已经被赋值的变量,引用类型变量可以是 null。
引用参数具有如下特征:
不会在栈上为形参分配内存。
形参的参数名将作为实参变量的别名,指向相同的内存位置。
class MyClass
{
// Initialize field to 20.
public int Val = 20;
}
class Program
{
// ref modifier ref modifier
static void MyMethod(ref MyClass f1, ref int f2)
{
f1.Val = f1.Val + 5;
f2 = f2 + 5;
// Add 5 to field of f1 param.
// Add 5 to second param.
Console.WriteLine($ "f1.Val: {f1.Val}, f2: {f2 }");
}
static void Main()
{
MyClass a1 = new MyClass();
int a2 = 10;
// ref modifiers
MyMethod(ref a1, ref a2);
// Call the method.
Console.WriteLine($ "a1.Val: {a1.Val}, a2: {a2 }");
}
}
6.13 引用类型作为值参数和引用参数
对于引用类型对象:
作为值参数传递:如果在方法内创建一个新对象并赋值给形参,对实参没有影响。
作为引用参数传递:如果在方法内创建一个新对象并赋值给形参,则实参也会随之改变。
将引用类型对象作为引用参数传递,目的是改变引用对象;
如果仅需改变引用类型对象的内容,只需值参数传递即可。
6.14 输出参数
输出参数用于从方法体内把数据传出到调用代码,要求如下:
在方法的声明和调用中都使用 out 修饰符。
实参必须是变量,使用前可以不赋值。
形参的参数名也作为实参变量的别名,指向同一块内存位置。
与 ref 不同,out 有如下要求:
给输出参数赋值之后,才能读取。
方法返回之前,必须给输出参数赋值。
从C# 7.0 开始,对输出参数进行简化声明,不需要预先声明一个变量来用作 out 参数了。
声明后的 a1 和 a2 可以在方法调用结束后继续使用。
static void Main()
{
MyMethod(out MyClass a1, out int a2 ); // Call the method.
Console.WriteLine(a2); // Use the return value
a2 += 5; // Write to the variable
Console.WriteLine(a2);
}
6.15 参数数组
参数数组允许特定类型的N个实参对应一个特定的形参,
参数数组使用:
参数列表中只能有一个参数数组,且必须为参数列表的最后一个。
参数数组中的所有参数类型必须相同。
参数数组的声明:
在形参前使用 params 修饰符,并在数据类型后放置一组方括号。
void ListInts(params int [] inVals)
{
// ...
}
6.15.1 方法调用
可以使用两种方式为参数数组提供实参:
使用逗号分隔的同数据类型的元素列表。
ListInts(10, 20, 30); // 3 个 int
一个同数据类型的一维数组。
int[] intArray = {1, 2, 3};
ListInts(intArray);
注意:在调用时不使用 params 修饰符。
延伸式
void ListInts(params int[] inVals) { ... } // 方法声明
// ...
ListInts(); // 0 个实参
ListInts(1, 2, 3); // 3 个实参
ListInts(4, 5, 6, 7, 8); // 5 个实参
使用独立实参调用时,编译器将执行以下步骤:
接受实参列表,在堆中创建并初始化一个数组;
把数组的引用保存到栈中的形参里;
如果没有实参(个数为 0),则编译器会创建一个具有 0 个元素的数组来使用。
当数组在堆中被创建时,实参的值被赋值到数组中,因此可看做值参数:
数组参数为值类型:值被复制,实参不受影响;
数组参数为引用类型:引用被复制,实参引用的对象在内部会受到影响。
6.15.2 将数组作为实参
编译器将使用传入的数组而不是重新创建一个。
static void Main()
{
// Create and initialize array.
int[] myArr = new int[] {5, 6, 7};
MyClass mc = new MyClass();
mc.ListInts(myArr); // Call method to print the values.
foreach(int x in myArr)
{
Console.WriteLine($ "{ x }"); // Print out each element.
}
}
6.16 参数类型总结
4种参数类型
参数类型 | 修饰符 | 是否在声明时使用 | 是否在调用时使用 | 执行 |
---|---|---|---|---|
值 | 无 | 系统把实参的值复制到形参 | ||
引用 | ref | 是 | 是 | 形参是实参的别名 |
输出 | out | 是 | 是 | 仅包含一个返回的值,形参是实参的别名 |
数组 | params | 是 | 否 | 允许传递可变数目的实参到方法 |
6.17 ref 局部变量和 ref 返回
ref局部变量,允许一个变量是另一个变量的别名。
创建别名的语法需要使用 ref 两次。
ref int y = ref x;
class Program
{
static void Main()
{
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}");
}
}
// output
x = 2, y = 2
x = 5, y = 5
x = 6, y = 6
ref 返回使得方法可以返回引用而不是值,同样也需要使用两次 ref:
class Simple
{
private int Score = 5;
// Keyword for ref return method
public ref int RefToValue()
{
// ref
return ref Score;
}
public void Display()
{
Console.WriteLine($ "Value inside class object: {Score}");
}
}
class Program
{
static void Main()
{
Simple s = new Simple();
s.Display();
// Keyword for ref local
ref int v1Outside = ref s.RefToValue();
// Change the value out in the calling scope.
v1Outside = 10;
s.Display();
}
}
// output
Value inside class object: 5
Value inside class object: 1
有关 ref 的使用有如下注意事项:
ref return 不能返回如下内容:
- 空值。
- 常量。
- 枚举成员。
- 类或结构体的属性。
- 指向只读位置的指针。
ref return 不能返回方法内部的局部变量;
ref 局部变量只能被赋值一次,后面出现的等号表示赋值;
如果调用 ref 返回方法时未使用 ref 关键字,则返回的是值而不是引用;
将 ref 局部变量作为常规的实际参数传递给其他方法时,传递的仍是 ref 指向的副本而不是引用。
6.18 方法重载
一个类中可以有多个同名方法,叫做方法重载。
使用相同名称的方法必须和其他同名方法有不同的签名,签名由如下信息组成:
- 方法名称。
- 参数数目。
- 参数的数据类型和顺序。
- 参数修饰符。
返回类型和形参名称都不是签名的一部分。
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; }
}
6.19 命名参数
C# 可以使用命名参数,显示指定参数的名称,就能够以任意顺序在方法中列出实参。
可以同时使用位置参数和命名参数,但所有位置参数必须先列出。
class MyClass
{
public int Calc(int a, int b, int c)
{
return (a + b) * c;
}
static void Main() {
MyClass mc = new MyClass();
// Positional Parameters
int r0 = mc.Calc(4, 3, 2);
// Positional and Named Parameters
int r1 = mc.Calc(4, b: 3, c: 2);
// Switch order
int r2 = mc.Calc(4, c: 2, b: 3);
// All named parameters
int r3 = mc.Calc(c: 2, b: 3, a: 4);
// Named parameter expressions
int r4 = mc.Calc(c: 2, b: 1 + 2, a: 3 + 1);
Console.WriteLine($ "{ r0 }, { r1 }, { r2 }, { r3 }, { r4 }");
}
}
6.20 可选参数
可选参数在调用方法时,可以忽略它,可选参数在声明时需要设置参数的默认值,
值类型的默认值在编译时可以确定,就可以作为可选参数
引用类型默认值为null时,可以作为可选参数。
所有必填参数需放在可选参数声明之前,params 参数放在可选参数之后,
可选参数使用规则:
必须从可选参数列表的最后向前开始省略,而不是任意省略参数。
如果需要任意省略参数,需要配合命名参数来实现以消除赋值的歧义。
class MyClass
{
double GetCylinderVolume(double radius = 3.0, double height = 4.0)
{
return 3.1416 * radius * radius * height;
}
static void Main()
{
MyClass mc = new MyCalss();
double volume;
volume = mc.GetCylinderVolume(3.0, 4.0); // 位置参数
volume = mc.GetCylinderVolume(radius: 2.0); // 使用 height 默认值
volume = mc.GetCylinderVolume(height: 2.0); // 使用 radius 默认值
volume = mc.GetCylinderVolume(); // 使用两个默认值
}
}
6.21 栈帧
调用方法时,内存从栈的顶部开始分配,保存和方法关联的一些数据项。这块内存称为方法的栈帧。
栈帧包含如下内容:
-
返回地址,即方法返回时继续执行的位置。
-
分配内存的参数,即值参数(参数数组,如果有的话)。
-
和方法调用相关的其他管理数据项。
在方法调用时,整个栈帧都会压入栈。
方法退出时,整个栈帧会从栈上弹出,也称栈展开。
6.22 递归
方法调用自身,叫做递归,需要注意终止条件,避免死循环。
class Program
{
public void Count(int inVal) {
if (inVal == 0)
return;
// Calls itself
// Invoke this method again.
Count(inVal - 1);
Console.WriteLine($ "{inVal }");
}
static void Main()
{
Program pr = new Program();
pr.Count(3);
}
}