目录
引言
看完了前五章感觉很不错,继续阅读!
方法
局部变量
字段通常保存和对象状态有关的数据,而创建局部变量经常是用于保存局部或临时的计算数据。
- 局部变量的存在和生存期仅限于创建它的块及其内嵌的块。
- 从声明它的那一点开始存在。
- 在块完成执行时结束存在。
- 可以在方法体内任意位置声明局部变量,但必须在使用它们前声明。
下给出实例字段和局部变量的对比表
实例字段 | 局部变量 | |
---|---|---|
生存期 | 从实例被创建开始,直到实例不再被访问时结束 | 从它在块中被声明那一刻开始,在块完成执行时结束 |
隐式初始化 | 初始化成该类型的默认值 | 没有隐式初始化。如果变量在使用之前没有被赋值,编译器就会产生一条错误信息 |
存储区域 | 由于实例字段是类的成员,所以所有字段都存储在堆里,无论它们是值类型的还是引用类型的 | 值类型:存储在栈里 引用类型:引用存储在栈里,数据存储在堆里 |
类型推断和var关键字
如果在声明变量的时候,可以推断出类型名,可以使用关键字var
来声明变量。
var
关键字并不是表示特殊变量。它只是语法上的速记,表示任何可以从初始化语句的右边推断出的类型。使用显式类型名的代码片段和使用var关键字的代码片段在语义上是等价的。
使用var
关键字有一些重要条件:
- 只能用于局部变量,不能用于字段;
- 只能在变量声明中包含初始化时使用;
- 一旦编译器推断出变量的类型,它就是固定且不能更改的。
var
关键字并不改变C#的强类型性质。
嵌套块中的局部变量
在C# 中不管嵌套级别如何,都不能在第一个名称的有效范围内声明另一个同名的局部变量。
局部常量
局部常量很像局部变量,只是一旦被初始化,它的值就不能改变了。如同局部变量,局部常量必须声明在块的内部。
常量的两个最重要特征:
- 在声明时必须初始化。
- 在声明后不能改变。
常量核心声明如下所示const Type Identifier = Value;
- 在类型之前增加关键字const(关键字const不是修饰符,而是核心声明的一部分。它必须直接放在类型的前面。)
- 必须有初始化语句。初始化值必须在编译期决定,通常是一个预定义简单类型或尤其组成的表达式。它还可以是null引用,但它不能是某对象的引用,因为对象的引用是在运行时决定的。
返回值
方法可以向调用代码返回一个值。返回的值被插入到调用代码中发起调用的表达式所在位置。
- 要返回值,方法必须在方法名面前声明一个返回类型。
- 如果方法不返回值,它必须声明void返回类型。
声明了返回类型的方法必须使用如下形式的返回语句从方法中返回一个值。返回语句包括关键字return
及其后面的表达式。每一条贯穿方法的路径都必须以一条这种形式的return
语句结束。
return Expresssion;
返回语句和void方法
在特定条件符合的时候,我们常常会提前退出方法以简化程序逻辑。
- 可以在任何时候使用下面的返回语句退出方法,不带参数:
return;
- 这种形式的返回语句只能作用于用void声明的方法。
局部函数
从C#7.0开始,你可以在一个方法中声明另一个单独的方法。这样可以将嵌入的方法跟其他代码隔离开来,所以它只能在包含它的方法内调用。
与局部变量必须在使用之前进行声明不同,你可以在包含方法的任意位置声明局部函数。
自己写了一个小例子:
int i = 1;
void asd()
{
ichange();
Console.Write(i);
int ichange()
{
return i++;
}
}
asd();
//output:2
参数
形参
形参是局部变量,它声明在方法的参数列表中,而不是在方法体中。
- 因为形参是变量,所以它们有类型和名称,并且能被写入和读取。
- 和方法中的其他局部变量不同,参数在方法体外面定义并在方法开始之前初始化(但有一种类型例外,称为输出参数)
- 参数列表中可以有任意数目的形参声明,而且声明必须用逗号隔开。
形参在整个方法体内使用,在大部分地方就像其他局部变量一样。
实参
当代码调用一个方法时,形参的值必须在方法的代码开始执行之前初始化。用于初始化形参的表达式或变量称作实参(actual parameter,有时也称argument)。
- 实参位于方法调用的参数列表中
- 每一个实参必须与对应形参的类型相匹配,或是编译器必须能把实参隐式转换为那个类型。
值参数
前文的参数类型是默认的类型,称为值参数(value parameter)
当你使用值参数时,通过将实参的值复制到形参的方式把数据传递给方法。方法被调用时,系统执行如下操作。
值参数的实参不一定是变量,它可以是任何能计算成相应数据类型的表达式。
- 在栈中为形参分配空间。
- 将实参的值复制给形参。
例子
internal class Myclass
{
public int Val = 20;
}
internal class Program
{
private static void MyMethod(Myclass f1, int f2)
{
f1.Val += 5;
f2 += 5;
Console.WriteLine($"f1.Val: {f1.Val}, f2: {f2}");
}
private static void Main()
{
Myclass a1 = new Myclass();
int a2 = 10;
MyMethod(a1, a2);
Console.WriteLine($"a1.Val: {a1.Val}, a2: {a2}");
}
}
结果:
f1.Val: 25, f2: 15
a1.Val: 25, a2: 10
引用参数
第二种参数类型称为引用参数。
- 使用引用参数时,必须在方法的声明和调用中都使用
ref
修饰符。 - 实参必须是变量,在用作实参前必须被赋值。如果是引用类型变量,可以赋值为一个引用或者null。
引用参数具有的特征:
- 不会在栈上为形参分配内存。
- 形参的参数名将作为实参变量的别名,指向想同的内存位置。
例子:
internal class Myclass
{
public int Val = 20;
}
internal class Program
{
private static void MyMethod(ref Myclass f1, ref int f2)
{
f1.Val += 5;
f2 += 5;
Console.WriteLine($"f1.Val: {f1.Val}, f2: {f2}");
}
private 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
简而言之就是引用参数的改变可以直接改变实参的值
引用类型作为值参数和引用参数
对于一个引用类型对象,不管是将其作为值参数传递还是作为引用参数传递,都可以在方法成员内部修改它的成员。不过,我们并没有在方法内部设置形参本身。本书来看看在方法内设置引用类型时会发生什么。
- 将引用类型对象作为值参数传递
如果在方法内创建一个新对象并赋值给形参,将切断形参与实参之间的关联,并且在方法调用结束后,新对象也不复存在。 - 将引用类型对象作为引用参数传递
如果在方法内创建一个新对象并赋值给形参,在方法结束后该对象依然存在,并且是实参所引用的值。
下面代码展示了第一种情况——将引用类型作为值函数传递:
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 mmethod call: {a1.Val}");
RefAsParameter(a1);
Console.WriteLine($"After mmethod call: {a1.Val}");
}
}
结果:
Before mmethod call: 20
After member assignment: 50
After new object creation: 20
After mmethod call: 50
解释:
- 在方法开始时,实参和形参指向堆中相同的对象。
- 在为对象的成员赋值之后,它们仍指向堆中想通的对象。
- 当方法分配新的对象并赋值给形参时,(方法外部的)实参仍指向原始对象,而形参指向的是新对象。
- 在方法调用之后,实参指向原始对象,形参和新对象都会消失。
下面将演示将引用对象作为引用参数的情况。除了方法声明和方法调用时要使用ref关键字外,与上面的代码完全相同:
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()
{
MyClass a1 = new MyClass();
Console.WriteLine($"Before mmethod call: {a1.Val}");
RefAsParameter(ref a1);
Console.WriteLine($"After mmethod call: {a1.Val}");
}
}
结果
Before mmethod call: 20
After member assignment: 50
After new object creation: 20
After mmethod call: 20
引用参数充当形参的别名。这样上面的代码就很好解释了。
- 在方法调用时,形参和实参指向堆中相同的对象。
- 对成员值的修改会同时影响到形参和实参。
- 当方法创建新的对象并赋值给形参时,形参和实参的引用都指向改新对象。
- 在方法结束后,实参指向方法内创建的新对象。
输出参数
输出参数用于从方法体内把数据传出到调用代码,它们的行为与引用参数类似。如同引用参数,输出参数有以下要求。
- 必须在声明和调用中都使用修饰符。输出参数的修饰符是
out
而不是ref
。 - 和引用参数相似,实参必须是变量,而不能是其他类型的表达式。这是有道理的,因为方法需要内存位置来保存返回值。
与引用参数类似,输出参数的形参充当实参的别名。形参和实参都是同一块内存位置的名称。显然, 在方法内对形参做的任何改变在方法执行完成之后(通过实参变量)都是可见的。
与引用参数不同,输出参数有以下要求。
- 在方法内部,给输出参数赋值之后才能读取它。这意味着参数的初始值是无关的,而且没有必要在方法调用之前为实参赋值。
- 在方法内部,在方法返回之前,代码中每条可能的路径都必须为所有输出参数赋值。
因为方法内的代码在读取输出参数之前必须对其写入,所以不可能使用输出参数把数据传入方法。
下面给出输出参数的例子:
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;
Console.WriteLine($"f1.Val: {f1.Val}, f2: {f2}");
}
static void Main()
{
Myclass a1 = null;
int a2;
MyMethod(out a1, out a2);
Console.WriteLine($"a1.Val: {a1.Val}, a2: {a2}");
}
}
结果
f1.Val: 25, f2: 15
a1.Val: 25, a2: 15
解释:
- 在方法调用之前,将要被用作实参的变量
a1
和a2
已经在栈里了。 - 在方法的开始,形参的名称被设置为实参的别名。你可以认为变量
a1
和f1
指向的是相同的内存位置,也可以认为a2
和f2
指向的是相同的内存位置。a1
和a2
不在作用域之内,所以不能在MyMethod
中访问。 - 在方法内部,代码创建了一个
MyClass
类型的对象并把它赋值给f1
。然后赋一个值给f1
的字段,也赋一个值给f2
。对f1
和f2
的赋值都是必须的,因为它们是输出参数。 - 方法执行之后,形参的名称已经失效,但是引用类型的
a1
和值类型的a2
的值都被方法内的行为改变了。
从C#7.0开始,你不再需要预先声明一个变量来用作out参数了。你可以在调用方法时在参数列表中添加一个变量类型,它将作为变量声明。
旧语法:
static void Main(){
MyClass a1=null; //声明将被用作out参数的变量
int a2; //声明将被用作out参数的变量
MyMethod(out a1,out a2); //调用方法
}
如果使用新语法,你可以:
- 消除显式的变量声明;
- 直接在方法调用时加入变量类型声明。
新语法:
static void Main(){
MyMethod( out MyClass a1, out int a2);//调用方法
}
虽然a1和a2只在方法调用语句中进行了声明,但它们也可以在方法调用完后继续使用。
参数数组
在前面的参数类型中,一个形参必须严格地对应一个实参。参数数组则不同,它允许特定类型的零个或多个实参对应一个特定的形参。参数的重点如下。
- 在一个参数列表中只能有一个参数数组。
- 如果有,它必须是列表中的最后一个。
- 由参数数组表示的所有参数必须是同一类型。
声明参数数组必须做的事如下。
- 在数据类型前使用
params
修饰符。 - 在数据类型后放置一组空的方括号。
下面的方法头展示了int
型参数数组达到声明语法。
void ListInt(params int[] inVals)
数组会在后面介绍,目前需要知道是:
- 数组是一组有序的同一类型的数据项。
- 数组使用一个数组类型索引进行访问。
- 数组是一个引用类型,因此它的所有数据项都保存在堆中。
方法调用
可以使用两种方式为参数数组提供实参。
- 一个用逗号分隔的该数据类型元素参数的列表。所有的元素必须是方法声明中指定的类型
ListInts(10,20,30); //三个int
- 一个该数据类型元素的一维数组。
int[] intArray = {1,2,3};
ListInts( intArray); //一个数组变量
这些实例中,没有在调用时使用params
修饰符。参数数组中修饰符的使用和其他参数类型的模式并不相符。
- 其他参数类型是一致的,要么都使用修饰符,要么都不使用修饰符。
- 值函数的声明和调用都不带修饰符。
- 引用参数和输出参数在两个地方都需要修饰符
params
修饰符的用法总结如下。- 在声明中需要修饰符。
- 在调用中不允许有修饰符。
延伸式
方法调用的第一种形式有时被称为延伸式,这种形式在调用中使用独立的实参。
例如:
void ListInts( params int[] inVals ){...} //方法声明
...
ListInts(); //0个实参
ListInts(1, 2, 3); //3个实参
ListInts(4, 5, 6, 7); //4个实参
ListInts(8, 9, 10, 11, 12); //5个实参
在使用一个为参数数组使用独立实参的调用时,编译器做下面几件事。
- 接受实参列表,用它们在堆中创建并初始化一个数组。
- 把数组的引用保存在栈的形参里。
- 如果在对应形参数组的位置没有实参,编译器会创建一个有零个元素的数组来使用。
例如,下面的代码声明了一个名为ListInts的方法,他接受一个有参数数组。Main声明了三个整数并把它们传递给了数组。
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}");
}
}
结果
50
60
70
5,6,7
解释
- 方法调用之前,3个实参已经在栈里。
- 在方法的开始,3个实参已经被用于初始化堆中的数组,并且数组的引用被赋值给形参
inVals
。 - 在方法内部,代码首先检查以确认数组引用是不是
null
,然后处理数组,把每个元素乘以10并保存回去。 - 方法执行之后,形参
inVals
失效
关于参数数组,需要记住的一点是当数组在堆中被创建时,实参的值被复制到数组中。这样它们很像值参数。
- 如果数组参数是值类型,那么值被复制,实参在方法内部不受影响。
- 如果数组参数是引用类型,那么引用被复制,实参引用的对象在方法内部会受到影响。
将数组作为实参
也可以在方法调用之前创建并组装一个数组,把单一的数组变量作为实参传递。这种情况下编译器使用你的数组而不是重新创建一个。
例子:
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}");
}
结果
50
60
70
50
60
70
小结
这里有一个小问题,就是我发现形参是可以和前面的局部变量重名的,那么形参既然是局部变量,他的范围是在哪?是不是在另一个空间,所以不会和前面的产生冲突
int i = 1;
void asd()
{
ichange(i);
Console.Write(i);
int ichange(int i)
{
return i++;
}
}
asd();
输出为 1
写太多了导致自动保存的时候巨卡,很影响写作体验,分为上下来写了。