方法的结构
方法是一块具有名称的代码。
可以使用方法的名称从别的地方执行代码,也可以把数据传入方法并接收数据输出。
方法是类的函数成员,主要有两个部分,方法头和方法体。
- 方法头 指定方法的特征
- 方法是否返回数据,若返回,返回什么类型
- 方法的名称
- 哪种类型的数据可以传递给方法或从方法返回,以及应如何处理这些数据
- 方法体 包含可执行代码序列
int MyMethod(int par1,string par2) ↑ ↑ ↑ 返回 方法 参数 类型 名称 列表
方法体内部代码的执行
方法体可包含以下项目
- 本地变量
- 控制流结构
- 方法调用
- 内嵌的块
static void Main() { int myInt = 3; //本地变量 while(myInt > 0) //控制流结构 { --myInt; PrintMyMessage(); //方法调用 } }
本地变量
与类的字段一样,本地变量也保存数据。字段通常保存和对象状态有关的数据,而本地变量通常用于保存本地的或临时的计算数据。
- 本地变量的存在性和生存期仅限于创建它的块及其内嵌的块
- 它从声明它的那一点开始存在
- 它在块完成执行时结束存在
- 可以在方法体内任意位置声明本地变量,但必须在使用前声明
类型推断和var关键字
观察下面的代码,你会发现编译器其实能从初始化语句的右边推断出来类型名。
- 第一个变量声明中,编译器能推断出15是int型
- 第二个变量声明中,右边的对象创建表达式返回了一个MyExcellentClass类型对象
所以在这两种情况中,在声明开始的显式的类型名是多余的。
static void Main() { int myInt = 15; MyExcellentClass mec = new MyExcellentClass(); ... }
为了避免这种冗余,可以在变量声明开始的显式类型名位置使用var关键字
static void Main() { var myInt = 15; var mec = new MyExcellentClass(); ... }
var不是特定的类型变量符号。它表示任何可以从初始化语句的右边推断出来的类型。
使用var有一些重要的条件
- 只能用于本地变量,不能用于字段
- 只能在变量声明中包含初始化时使用
- 一旦编译器推断出变量的类型,它就是固定且不能更改的
说明:var关键字不像JavaScript的var那样可以引用不同的类型。它是从等号右边推断出的实际类型的速记。var关键字并不改变C#的强类型性质。
嵌套块中的本地变量
方法体内部可以嵌套其他的块
- 可以有任意数量的块,并且它们既可以是顺序的也可以更深层嵌套。
- 本地变量可以在内嵌块内部声明,并且和所有本地变量一样,它的生存期和可见性仅限于声明它们的块及其内嵌块
说明:在C和C++中,可以先声明一个本地变量,然后在嵌套块中声明另一个同名本地变量。在内部范围,内部变量覆盖外部变量。然而,在C#中不管嵌套级别如何,都不能在第一个本地变量的有效范围内声明另一个同名本地变量。
本地常量
本地常量一旦被初始化就不能改变了,且必须声明在块的内部
常量的两个重要特征
- 常量在声明时必须初始化
- 常量在声明后不能改变
常量声明语法
关键字 ↓ const Type Identifier = Value; ↑ 初始化值是必须的
控制流
控制流指的是程序从头到尾的执行流程。
默认情况下,程序从上到下执行,控制流语句允许你改变执行顺序。
- 选择语句 选择哪条语句或语句块来执行
- if 判断true则执行
- if…else true执行if,false执行else
- switch 在一组语句中执行某一条
- 迭代语句 在一个语句块上循环或迭代
- for 循环-顶部判断循环条件
- while 循环-顶部判断循环条件
- do 循环-底判断循环条件
- foreach 一组中每个成员执行一次
- 跳转语句 在代码块或方法体内部跳转
- break 跳出当前循环
- continue 到当前循环底部
- goto 到一个指定的语句
- return 返回调用方法继续执行
void SomeMethod() { int intVal = 3; if(intVal == 3) { Console.WriteLine("Value is 3."); } for(int i=0;i<5;i++) { Console.WriteLine("Value of i:{0}",i); } }
方法调用
可以从方法体的内部调用(call/invoke)其它方法
调用方法时要使用方法名并带上参数列表
- 当前方法的执行在调用点被挂起
- 控制转移到被调用方法的开始
- 被调用方法执行直到完成
- 控制回到发起调用的方法
返回值
方法可以向调用代码返回一个值
- 要返回值,方法必须在方法名前面声明一个返回类型
- 如果代码不返回值,它必须声明返回类型为void(空)
- 声明了返回类型的,通过return语句返回值
返回类型 ↓ int GetHour() { DateTime dt = DateTime.Now; int hour = dt.Hour; //获取当前小时 return hour; ↑ 返回语句 }
也可以返回用户定义类型的对象
返回类型---MyClass ↓ MyClass method3() { MyClass mc = new MyClass(); ... return mc; }
返回语句和void方法
- 可以在任何时候使用下面形式的语句退出方法,不带参数
return
- 这种形式的返回语句只能用于void声明的方法
例
- 方法获取当前日期和时间
- 如果小时小于12,那么执行return语句,不在屏幕上输出任何东西,直接把控制返回给调用方法
- 如果小时大于等于12,则跳过return语句,代码执行WriteLine语句,在屏幕上输出信息
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(); } }
参数
参数允许你在方法开始执行时把数据传入方法,或是在一个方法体中返回多个返回值。
形参
形参是本地变量,它声明在方法的参数列表中,而不是在方法体中
public void PrintSum(int x, float y) { ↑ ... 形参声明 }
- 因为形参是变量,所以它们有类型和名称,并能被写入和读取
- 和方法体中的其他本地变量不同,参数在方法体的外面定义并在方法开始前初始化(输出参数除外)
- 参数列表中可以有任意数目的形参声明,而且声明必须用逗号隔开
形参在整个方法体内使用,在大部分地方就像其他本地变量一样
实参
- 用于初始化形参表达式或变量称作实参
- 实参位于方法调用的参数列表中
- 每一个实参必须与对应形参的类型相匹配,或是编译器必须能够把实参隐式转换为那个类型
第二次调用,编译器把int 5 和 someInt隐式转换成了float
值参数
使用值参数,通过将实参的值复制到形参的方式把数据传递给方法。方法被调用时,系统做如下操作
- 在栈中为形参分配空间
- 将实参的值复制给形参
你应该记得第3章介绍了值类型,所谓值类型就是指类型本身包含其值。不要把值类型和这里介绍的值参数混淆,它们是完全不同的两个概念。值参数是把实参的值复制给形参。
class MyClass { public int Val=20; } class Program { static void MyMethod(MyClass f1,int f2) { f1.Val=f1.Val+5; f2=f2+5; Console.WriteLine("f1.val:{0},f2:{1}",f1.Val,f2); } static void Main() { MyClass a1=new MyClass(); int a2=10; MyMethod(a1,a2); Console.WriteLine("f1.Val:{0},f2:{1}",a1.Val,a2); } }
- 在方法被调用前,用作实参的变量a2已经在栈里了
- 方法开始时,系统在栈中为形参分配空间,并从实参复制值
- 因为a1是引用类型,所以引用被复制,结果实参和形参都引用堆中同一对象
- 因为a2是值类型,所以值被复制,产生了一个独立的数据项
- 方法的结尾,f2和对象f1的字段都被加上5
- 方法执行后,形参从栈中弹出
- a2 值类型,它的值不受方法行为的影响
- a1 引用类型,它的值被方法的行为改变了
引用参数
- 使用引用参数,必须在方法的声明和调用时都使用ref修饰符
- 实参必须是变量,在用作实参前必须被赋值。如果是引用类型变量,可以赋值为一个引用或null
包含ref修饰符 ↓ void MyMethod(ref int val) { ... } int y = 1; MyMethod(ref y); ↑ 包含ref修饰符 MyMethod(ref 3+5); //报错 ↑ 必须使用变量
对于值参数,系统在栈上为形参分配内存,引用参数则不同
- 不会为形参在栈上分配内存
- 实际情况是:形参的参数名将作为实参变量的别名,指向相同的内存位置
由于形参名和实参名的行为就好像指向相同内存位置,所以在方法的执行过程中对形参做的任何改变在方法完成后依然有效
clas MyClass { public int Val = 20; } class Program { ref修饰符 ref修饰符 ↓ ↓ static void MyMethod(ref MyClass f1,ref int f2) { f1.Val=f1.Val+5; f2=f2+5; Console.WriteLine("f1.Val:{0},f2:{1}",f1.Val,f2); } static void Main() { MyClass a1=new MyClass(); int a2 =10; ref修饰符 ↓ ↓ MyMethod(ref a1,ref a2); Console.WriteLine("f1.Val:{0},f2:{1}",a1.Val,a2); } }
- 方法调用前,将要被用作实参的变量a1和a2已经在栈里了
- 方法开始,形参名被设置为实参的别名。变量a1和f1引用相同的内存位置,a2和f2引用相同的内存位置
- 在方法结束位置,f2和f1的对象字段都被加上了5
- 方法执行后,形参的名称已经失效,但是值类型a2的值和引用类型a1所指向的对象的值都被方法内的行为改变了
引用类型作为值参数和引用参数
从前几节看到,对于引用类型对象,不管是将其作为值参数传递还是引用参数传递,我们都可以在方法成员内部修改它的成员。不过我们并没有在方法内部修改形参本身。本节来看看方法内修改引用类型形参会发生什么。
- 将引用类型对象作为值参数传递:如果在方法内创建一个新对象并赋值给形参,将切断形参与实参间的关联,并且在方法调用结束后,新对象也将不复存在。
- 将引用类型对象作为引用参数传递:如果在方法内创建一个新对象并赋值给形参,在方法结束后该对象依然存在,并且是实参所引用的值
例:将引用类型对象作为值参数传递
将引用类型对象作为值参数传递
class MyClass{public int Val=20;} class Program { static void RefAsParameter(MyClass f1) { f1.Val=50; Console.WriteLine("After meber assignment:{0}",f1.Val); f1=new MyClass(); Console.WriteLine("After new object creation:{0}",f1.Val); } static void Main() { MyClass a1=new MyClass(); Console.WriteLine("Before method call:{0}",a1.Val); RefAsParameter(a1); Console.WriteLine("After method call:{0}",a1.Val); } }
- 在方法开始时,实参和形参都指向堆中相同的对象
- 在为对象的成员赋值后,它们仍指向堆中相同的对象
- 当方法分配新的对象并赋值给形参时,(方法外部的)实参仍指向原始对象,而形参指向的是新对象
- 在方法调用后,实参指向原始对象,形参和新对象消失
例:将引用类型对象作为引用参数传递
class MyClass{public int Val=20;} class Program { static void RefAsParameter(ref MyClass f1) { f1.Val=50; Console.WriteLine("After meber assignment:{0}",f1.Val); f1=new MyClass(); Console.WriteLine("After new object creation:{0}",f1.Val); } static void Main(string[] args) { MyClass a1=new MyClass(); Console.WriteLine("Before method call:{0}",a1.Val); RefAsParameter(ref a1); Console.WriteLine("After method call:{0}",a1.Val); } }
引用参数的行为就像是将实参作为形参的别名。
- 在方法调用时,形参和实参都指向堆中相同的对象
- 对成员值的修改会同时影响到形参和实参
- 当方法创建新的对象并赋值给形参时,形参和实参的引用都指向该新对象
- 在方法结束后,实参指向在方法内创建的新对象
输出参数
输出参数用于从方法体内把数据传出到调用代码,它们的行为与引用参数非常类似。
输出参数有以下要求
- 必须在声明和调用中都使用 out 修饰符
- 和引用参数类似,实参必须是变量
- 在方法内部,输出参数在被读取前必须赋值
- 方法返回前,方法内任何返回路径都必须为所有输出参数进行赋值
class MyClass { public int Val=20; } class Program { static void MyMethod(out MyClass f1,out int f2) { f1=new MyClass(); f1.Val=25; f2=15; } static void Main() { MyClass a1=null; int a2; MyMethod(out a1,out a2); } }
- 方法调用前,将作为实参的变量a1和a2已经在栈里了
- 方法的开始,形参的名称设置为实参的别名。你可以认为变量a1和f1指向相同的内存位置,a2和f2指向相同内存位置。a1、a2不在作用域内,所以不能在MyMethod中访问
- 方法内部,对f1和f2的赋值是必需的,因为它们是输出参数
- 方法执行后,形参名称失效,但是引用类型的a1和值类型a2的值都被方法内的行为改变
参数数组
上述的参数类型都必须严格地一个实参对应一个形参。参数数组则不同,它允许零个或多个实参对应一个特殊的形参
- 在一个参数列表中只能有一个参数数组
- 如果有,它必须是列表中最后一个
- 由参数数组表示的所有参数都必须具有相同类型
声明参数数组必须做的事如下
- 在数据类型前使用 params 修饰符
- 在数据类型最后放置一组空的方括号
例:int型参数数组声明语法
void ListInts(params int[] inVals)
{...}
- 数组是一组整齐的同类型数据项
- 数组使用一个数字索引进行访问
- 数组是引用类型,它的所有数据项都存在堆中
方法调用
可以使用两种方式为参数数组提供实参
- 逗号分隔的该数据类型元素列表
ListInts(10,20,30); - 该数据类型元素的一维数组
int[] intArray={1,2,3};
ListInts(intArray);
在使用一个为参数数组分离实参的调用时,编译器做下面的事
- 接受实参列表,用它们在堆中创建并初始化一个数组
- 把数组的引用保存在栈中的形参里
- 如果在对应的形参数组位置没有实参,编译器会创建一个有零个元素的数组来使用
class MyClass { public void ListInts(params int[] inVals) { if((inVals!=null)&&(inVlas.Length!=0)) { for(int i=0;i<inVals.Length;i++) { inVals[i]=inVals[i]*10; Console.WriteLine("{0}",inVals[i]); } } } } class Program { static void Main() { int first=5,second=6,third=7; MyClass mc=new MyClass(); mc.ListInts(first,second,third); Console.WriteLine("{0},{1},{2}",first,second,third); } }
- 方法调用前,3个实参已经在栈里
- 方法开始,3个实参被用于初始化堆中的数组,并且数组的引用被赋值给形参inVals
- 方法内部,代码首先检查以确认数组引用不是null,然后处理数组,把每个元素乘以10并保存回去
- 方法执行后,形参inVals失效
关于参数数组,需记住重要的一点是当数组在堆中被创建时,实参的值被复制到数组中。它们就像值参数。
- 如果数组参数是值类型,那么值被复制,实参不受方法内部影响
- 如果数组参数是引用类型,那么引用被复制,实参引用的对象可以受到方法内部的影响
用数组作为实参
直接把数组变量作为实参传递,这种情况下,编译器使用你的数组而不是重新创建一个。
参数类型总结
方法重载
一个类中可以用一个以上的方法拥有相同名称,这叫方法重载(method overload)。使用相同名称的方法必须有一个和其他方法不同的签名(signature)
- 方法的签名由下列信息组成,它们在方法声明的方法头中
- 方法的名称
- 参数的数目
- 参数的数据类型和顺序
- 参数修饰符
- 返回类型不是签名的一部分
- 形参名称也不是签名的一部分
例:4个AddValue的重载
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;} }
例:错误的重载
class B { long AddValues(long a,long b){return a+b;} int AddValues(long c,long d){return c+d;} }
命名参数
至今我们所用到的参数都是位置参数,每个实参的位置都必须与相应的形参位置一一对应。
C#允许我们使用命名参数(named parameter),只要显式指定参数名字,就可以以任意顺序在方法调用中列出实参
- 方法的声明没有什么不一样。形参已经有名字
- 不过调用方法时,形参的名字后面跟着冒号和实际的参数值或表达式
例:使用命名参数的结构
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("{0},{1},{2},{3},{4}",r0,r1,r2,r3,r4); } }
代码输出
命名参数对于自描述程序来说很有用,我们可以在方法调用时显示那个值赋给那个形参。
例:使用命名参数 增强程序易读性
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.GetCylindreVolume(3.0,4.0); ... volume = mc.GetCylindreVolume(radius:3.0,height:4.0) } }
可选参数
可选参数就是我们可以在调用方法时包含这个参数,也可以省略。
为了表名某参数可选,你需要在方法声明时为参数提供默认值
- 给形参b设置默认值3
- 因此,若调用方法时只有一个参数,方法会使用3作为第二个参数的初始值
class MyClass { public int Calc(int a ,int b=3) { return a+b; } static void Main() { MyClass mc=new MyClass(); int r0=mc.Calc(5,6); int r1=mc.Calc(5); Console.WriteLine("{0},{1}",r0,r1); } }
不是所有参数类型都可以作为可选参数
- 只要值类型的默认值在编译时可以确定,就可以使用值类型作为可选参数
- 只有在默认值是null时,引用类型才可以作为可选参数
-
所有必填参数必须在可选参数前声明。如果有params参数,必须在可选参数后声明。
当有多个可选参数时,默认情况下只能省略后面几个
- 你必须从可选参数列表的最后开始省略,一直到列表开头
- 即你可以省略最后一个或n个可选参数,但不可以随意选择省略任意的可选参数
class MyClass { public int Calc(int a=2,int b=3,int c=4) { return (a+b)*c; } static void Main() { MyClass mc=new MyClass(); int r0=mc.Calc(5,6,7); int r1=mc.Calc(5,6); int r2=mc.Calc(5); int r3=mc.Calc(); Console.WriteLine("{0},{1},{2},{3}",r0,r1,r2,r3); } }
当有多个可选参数时,可以通过参数名字来选择可选参数
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.GetCylindervoume(3.0,4.0)://位置参数 Condole.WriteLine("Volume="+volume); volume =mc.GetCylindervoume(radius:2.0)://使用hieght默认参数 Condole.WriteLine("Volume="+volume); volume =mc.GetCylindervoume(height:2.0)://使用radius默认参数 Condole.WriteLine("Volume="+volume); volume =mc.GetCylindervoume()://使用默认值 Condole.WriteLine("Volume="+volume); } }
栈帧
至此,我们已经知道局部变量和参数是位于栈上的,再来深入探讨一下其组织。
调用方法时,内存从栈顶开始分配,保存和方法关联的一些数据项。这块内存叫做方法的栈帧(stack frame)。
- 栈帧内存包含以下内容
- 返回地址,即方法退出时继续执行的位置
- 这些参数分配的内存,也就是方法的值参数,或参数数组
- 各种和方法调用相关的其他管理数据项
- 在方法调用时,整个栈帧会压入栈
- 在方法退出时,整个栈帧从栈上弹出。弹出栈帧也叫栈展开(unwind)
例:下面代码声明了3个方法。Main调用MethodA,MethodA调用MethodB,创建了3个栈帧。方法退出时,栈展开。
class Program { static void MethodA(int par1,int par2) { Console.WriteLine("Enter MethodA:{0},{1}",par1,par2); MethodB(11,18); Console.WriteLine("Exit MethodA"); } static void MethodB(int par1,int par2) { Console.WriteLine("Enter MethodB:{0},{1}",par1,par2); Console.WriteLine("Exit MethodB"); } static void Main() { Console.WriteLine("Enter Main"); MethodA(15,30); Console.WriteLine("Exit Main"); } }
调用方法时栈帧压栈和栈展开的过程
递归
除了调用其他方法,方法也可以调用自身。这就是递归。
递归会产生很优雅的代码。
class Program { public void Count(int inVal) { if(inVal==0) { return; } else { Count(inVal-1); Console.WriteLine("{0}",inVal); } } static void Main() { Program pr=new Program(); pr.Count(3); } }