C#之方法
在C#中,方法是类的函数成员,方法由两个主要部分:
(1)方法头:指定了方法的特征,包括是否返回数据,如果返回,返回什么类型;方法的名称;哪种类型的数据可以传递给方法或从方法返回,以及如何处理这些数据.
(2)方法体:包含可执行代码的语句序列.
本地变量
与类的字段一样,本地变量也保存数据,字段通常保存和对象状态有关的数据,而创建本地变量经常是用于保存本地的或临时的计算数据.
实例字段 | 本地变量 | |
---|---|---|
生存期 | 从实例被创建时开始,到实例不再被访问时结束 | 从它在块中被声明开始,在块完成执行时结束 |
隐式初始化 | 初始化该类型的默认值 | 没有隐式初始化,如果变量在使用之前没有被赋值,编译器会产生一条错误信息 |
存储区域 | 由于实例是类成员,所以所有的字段都存储在堆里,无论它们是值类型还是引用类型 | 值类型:栈 引用类型:引用存储在栈,数据存储在堆. |
类型推断和var关键字
在本地变量声明中,可以使用var
关键字进行类型推断,比如
static void Main(){
var total=15;
var mec=new MyExcellentClass();
}
var
表示可以从初始化语句的右边推断出的类型,第一个声明中,它是int
的速记,第二个声明中,它是MyExcellentClass
的速记。需要注意的还有:只能在变量声明中包含初始化时使用,一旦编译器推断出变量的类型,它就是固定且不能更改的。
本地常量
常量最重要的两个特征,一是在声明时必须初始化,二是在声明后不能改变。
const Type Identifier=Value;
const 不是一个修饰符,而是核心声明中的一部分,它必须放在类型的前面。
参数
形参
形参是本地变量,它声明在方法的参数列表中,而不是在方法体中,它在整个方法体内使用。
如:
public void PrintSum(int x, float y) //形参声明
{
...
}
实参
当代码调用一个方法时,形参的值必须在方法的代码开始执行之前就被初始化。(有一种类型例外,称为输出参数)。用于初始化形参的表达式或变量称为实参,实参位于方法调用的参数列表中,每个实参必须与对应的形参的类型相匹配,或是编译器必须能够把实参隐式转换为那个类型。当方法被调用时,每个实参的值都被用于初始化相应的形参,方法体随后被执行。
- 位置参数
位置参数要求实参的数量必须与形参的数量一致,并且每个实参的类型与所对应的形参类型一致。
namespace LearningCSharp
{
class MyClass
{
public int Sum(int x,int y) //形参
{
return x + y;
}
public float Avg(float input1,float input2) //形参
{
return (input1 + input2) / 2;
}
}
class Program
{
static void Main()
{
MyClass myT = new MyClass();
int someInt = 6;
Console.WriteLine("Newflash:sum:{0} and {1} is {2}", 5, someInt, myT.Sum(5, someInt)); //调用方法中的实参
Console.WriteLine("Avg:{0} and {1} is {2}", 5, someInt, myT.Avg(5, someInt)); //调用方法中的实参,第二次调用编译器把int值隐式的转换为float值
}
}
}
值参数
使用值参数,通过将实参的值复制到形参的方法把数据传递给方法,方法被调用的时,系统在栈中为形参分配空间,将实参的值复制给形参,所以对于值类型来讲,值参数并不会改变其值,对于引用类型,值参数可能会改变其值。
值参数的实参不一定是变量,它可以是任何能计算成相应数据类型的表达式。
把变量用作实参之前,变量必须赋值(除非是输出参数,但输出参数虽然不要求实参必须赋值,但要求方法内的形参在被读取前,必须在方法内完成一次赋值),对于引用类型,变量可以被设置成为一个实际的引用或null。
需要注意的是:所谓值参数和值类型是两个概念,值类型指的是其值被记录在栈上,而值参数是将实参的值复制给形参(如果值参数类型是值类型,那么在栈上复制一个相同的值,如果值参数类型是引用类型,则在栈上把实参的引用复制给形参作引用)
namespace LearningCSharp
{
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);
}
}
}
在方法调用前,用作实参的a1,a2都在栈中分配了空间(必须);方法开始时,系统在栈中为形参分配空间,并从实参复制值,因为a1是引用类型,所以引用被复制,都引用堆中的同一个对象;在方法的结尾,f1与f2的字段都被加上了5,方法执行完毕,形参从栈中弹出,a2值类型不受方法的影响。
引用参数
使用引用参数时,必须在方法的声明和调用时都使用ref
修饰符,与值参数不同的是,实参必须是变量,在用作实参前必须被赋值,如果是引用类型变量,可以赋值为一个引用或null。
上图可以看到,对于引用参数来讲,实参必须是变量,而不能是表达式。
对于引用参数,系统不会为形参在栈上分配空间,而是形参的参数名作为实参变量的别名,指向相同的内存位置。这样对值类型实参来讲,函数就有可能改变其值。
namespace LearningCSharp
{
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;
Console.WriteLine("f1.Val:{0},f2:{1}", f1.Val, f2);
}
static void Main()
{
MyClass a1 = new MyClass();
int a2 = 10;
MyMethod(ref a1,ref a2 ); //对于应用参数,实参也必须先赋值
Console.WriteLine("f1.Val:{0},f2:{1}", a1.Val, a2);
}
}
}
在方法调用之前,将要被用作实参的变量a1,a2已经在栈里了;在方法的开始,形参名被设置为实参的别名,变量a1和f1引用相同的内存位置,a2和f2引用相同的内存位置;在方法结束位置,f2和f1的对象的字段都被加上了5.
引用类型作为值参数和引用参数,并且在方法内部创建一个新对象赋值给形参
- 将引用类型对象作为值参数传递,因为实参和形参的引用都在栈上,且是分配了不同的空间,如果在方法内创建一个新对象并赋值给形参,那么形参就引用了该新的对象,而实参的引用没变,所以将切断形参和实参的联系,并在方法调用后,形参被弹出栈,新对象也就不复存在。
namespace LearningCSharp
{
class MyClass
{
public int Val = 20;
}
class Program
{
static void MyMethod( 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}");
MyMethod(a1 );
Console.WriteLine($"After method call:{a1.Val}");
}
}
}
- 将引用类型对象作为引用参数传递,因为实参和形参都是栈上同一个空间,互为彼此的别名,也引用了同一堆上的对象,若在方法内新创建一个新对象,并赋值给形参,那么形参和实参也都将引用该新对象,因此在方法结束后,该对象仍然存在,并且是实参所引用的值。
namespace LearningCSharp
{
class MyClass
{
public int Val = 20;
}
class Program
{
static void MyMethod(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()
{
MyClass a1 = new MyClass();
Console.WriteLine($"Before method call:{a1.Val}");
MyMethod(ref a1 );
Console.WriteLine($"After method call:{a1.Val}");
}
}
}
输出参数
输出参数必须在声明和调用中都使用out
修饰符,和引用参数类似,实参必须是变量,不能 是其他类型的表达式,输出参数的形参也担任了实参的别名,与引用参数不同的是:在方法内部,输出参数在读取之前必须被赋值,这意味着实参初始值的有无对输出参数没有影响;在方法返回之前,方法内部必须至少为输出参数赋值一次。
输出参数这个特点也是可以被理解的,它设计的目的就是输出一些参数,因此实参没必要进行初始化,但形参必须在方法内完成初始化,否则就算实参进行了初始化,在方法体内,形参没有初始化,方法内读取形参,仍然会报错。
namespace LearningCSharp
{
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()
{
int a2 = 4;
MyClass a1 = null;
MyMethod(out a1, out a2);
Console.WriteLine($"{a1.Val},{a2}");
}
}
}
参数数组
参数数组允许零个或多个实参对应一个特殊的形参.需要关注的重点如下:
- 在一个参数列表中只能有一个参数数组.
- 如果有,它必须是列表中的最后一个
- 由参数数组表示的所有参数都必须具有相同的类型
声明一个参数数组的语法如下:
params int[] intVals
其中,params是修饰符,数据类型后需要放置一组空的方括号.
方法调用
可以使用两种方式为参数数组提供实参:
- 一个逗号分割的该数据类型元素的列表.所有元素必须是方法声明中指定的类型.
ListInts(10,20,30);
- 一个该数据类型元素的一维数组
int[] intArray={1,2,3};
ListInts(intArray);
注意到调用时没有使用params
修饰符.
namespace LearningCSharp
{
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;
MyClass mc = new MyClass();
mc.ListInts(first, second, third);
Console.WriteLine($"{first},{second},{third}");
}
}
}
用数组作为实参
namespace LearningCSharp
{
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[] myArr = { 4, 5, 6 };
MyClass mc = new MyClass();
mc.ListInts(myArr);
foreach (int x in myArr)
Console.WriteLine(x);
}
}
}
对比以上两个代码片段,第一个是通过调用分离的实参,第二个是通过调用数组:
当数组在堆中创建,实参的值被复制到数组中,如果数组参数是值类型,那么值被复制,实参不受方法影响;如果数组参数是引用类型,那么该引用被复制,实参的引用对象可以受到方法内部的影响.
方法重载
一个类中可以有一个以上的方法拥有相同的名称,这叫做方法重载.使用相同名称的没个方法必须有一个和其他方法不同的签名.所谓签名是:
方法的名称,参数的数目,参数的数据类型和顺序,参数的修饰符.
返回类型和形参名称不是签名的一部分.
命名参数
每个实参的位置都必须与相应的形参的位置一一对应的参数称为位置参数,除此之外,C#还允许我们使用命名参数,只要显式指定参数的名字,就可以以任意顺序在方法调用中列出实参.
命名参数在调用中采用形参名称:实参值
的方式.
namespace LearningCSharp
{
class MyClass
{
public int Calc(int a,int b,int c)
{
return (a + b) * c;
}
}
class Program
{
static void Main()
{
MyClass mc = new MyClass();
int result = mc.Calc(c: 2, a: 4, b: 3);
Console.WriteLine($"{result}");
}
}
}
可选参数
为表明某个参数是可选的,需要在方法声明的时候为参数提供默认值.
namespace LearningCSharp
{
class MyClass
{
public int Calc(int a,int b,int c=3)
{
return (a + b) * c;
}
}
class Program
{
static void Main()
{
MyClass mc = new MyClass();
int result = mc.Calc( a: 4, b: 3);
Console.WriteLine($"{result}");
}
}
}
-
不是所有的参数类型都可以作为可选参数,事实上,只有值参数的值类型和值参数的只允许null的默认值的引用类型才可以有可选参数.
-
所有必填参数必须在可选参数声明之前声明,如果有参数数组,必须在所有可选参数之后声明.
(intx,decimal y,...int op1=17,double op2=36,....params int[] intVals)
//必填参数.....可选参数.....params参数