类型推断和var 关键字
● 可以使用关键字 var, 来表示任何可以从初始化语句的右边推断出的类型,(类似于C++11 中的auto 语句)。
使用var关键字注意的有:
只能用于本地变量,不能用于数据成员的声明,和成员函数的声明。
只能在变量声明中包含初始化时使用。
一旦编译器推断出变量的类型,它就是固定且不能更改的。
namespace Ch05Ex03
{
class Program
{
public var mydad(int o); //错误
public var mydd=19; //错误
static void Main(string[] args)
{
var ival = 15;
ival = 17;
var jiji; //错误
WriteLine(ival);
ReadKey();
}
}
}
● 在C和C++中,可以声明一个本地变量,然后在嵌套块中声明另一个相同名称的本地变量。在内部范围,内部的名称隐藏了外部的名称。
在C#中不管嵌套级别如何,都不能在第一个名称的有效范围内声明另一个同名的本地变量。
值参数
● 使用值参数,通过将实参的值复制到形参的方式把数据传递给方法。 在方法调用时,系统做如下操作:
在栈中为形参分配空间。
将实参的值复制给形参。
● 值参数的实参不一定是变量,也可以是任何能计算成相应数据类型的表达式。
● 注意: 在把变量用作实参之前 ,变量必须被赋值(除非是输出参数),对于引用类型,变量可以设置为一个实际的引用或者null.
● 注意:值类型就是指类型本身包含其值。 值参数是把实参的值复制给形参。
●注意: 如果实参是引用类型的,并且使用值传递,当函数调用时, 引用被复制,结果实参和形参都引用堆中的同一个对象。
引用参数
● 使用引用参数时, 必须在函数的声明中和调用中都使用ref。
● 实参必须是变量,在用作实参前该变量必须被赋值。 如果该实参是引用类型变量, 可以赋值为一个引用 或者 null。
● 对于值传递,系统在栈上为形参分配内存,引用参数具有以下特征:
不会在栈上分配内存
实际情况是,形参的参数名将作为实参变量的别名,指向相同的位置。
引用类型作为值参数和引用参数
● 对于一个引用类型对象,不管是将其作为值参数传递还是作为引用参数传递,我们都可以在方法成员内部修改它的值。
不过它们还是有区别的的:
如果是值传递引用类型对象,当函数调用时,系统在栈上为形参分配内存,结果实参和形参都引用堆中的同一个对象。 意识就是说,有两个独立的变量指向同一个对象。
如果是引用传递类型对象,当函数调用时,系统不会为形参在栈上分配内存,实际情况是,形参的参数名将作为实参变量的别名,指向相同的位置。 意识就是说,形参和实参共用一块内存,它们都指向同一个对象。
● 如果我们在方法的内部设置形参本身,将引用类型对象作为值传递或者引用传递、将发生不同的行为:
如果将引用类型对象作为值传递,如果在方法的内部创建一个新对象并赋值给形参,将切断形参和实参之间的关联, 并且在方法调用结束后,新对象也将不复存在。
将引用类型对象作为引用传递, 如果在方法的内部创建一个新对象并赋值给形参, 在方法调用结束后,新对象依然存在,并且是实参所引用的值。
如果将引用类型对象作为值传递的代码:
namespace Ch05Ex03
{
class MyClass
{
public int Val = 20;
}
class Program
{
static void RefAsParameter(MyClass f1)
{
f1.Val = 50;
WriteLine($"赋值之后的成员数据为:{f1.Val}");
f1 = new MyClass();
WriteLine($"创建一个新对象的数据成员为:{f1.Val}");
}
static void Main(string[] args)
{
MyClass a1 = new MyClass();
WriteLine($"调用函数之前的数据为:{a1.Val}");
RefAsParameter(a1);
WriteLine($"调用函数之后的数据为:{a1.Val}");
ReadKey();
}
}
}
输出结果为:
调用函数之前的数据为:20
赋值之后的成员数据为:50
创建一个新对象的数据成员为:20
调用函数之后的数据为:50
当方法分配新的对象并赋值给形参时,方法外部的实参仍指向原始对象,而形参指向的是新对象。
在方法调用之后,实参指向原始对象,形参和新对象都会消失。
如果将引用类型对象作为引用传递的代码:
namespace Ch05Ex03
{
class MyClass
{
public int Val = 20;
}
class Program
{
static void RefAsParameter(ref MyClass f1)
{
f1.Val = 50;
WriteLine($"赋值之后的成员数据为:{f1.Val}");
f1 = new MyClass();
WriteLine($"创建一个新对象的数据成员为:{f1.Val}");
}
static void Main(string[] args)
{
MyClass a1 = new MyClass();
WriteLine($"调用函数之前的数据为:{a1.Val}");
RefAsParameter(ref a1);
WriteLine($"调用函数之后的数据为:{a1.Val}");
ReadKey();
}
}
}
输出结果为:
调用函数之前的数据为:20
赋值之后的成员数据为:50
创建一个新对象的数据成员为:20
调用函数之后的数据为:20
因为引用参数的行为就像是将实参做为形参的别名:
在方法调用时,形参和实参都指向堆中相同的对象。
对成员值的修改会同时影响到形参和实参。
当方法创建新的对象并赋值给形参时,形参和实参的引用都指向该新对象。
在方法结束后,实参指向在方法内创建的新对象。
输出参数
● 输出参数用于从方法体内把数据传出到调用代码,它的行为类似于引用参数。 使用输出参数注意:
必须在声明和调用中都使用修饰符 out。
和引用参数类似,实参必须是变量,而不能是其他类型的表达式。这是因为方法需要内存位置保存返回值。
● 与引用参数类似,输出参数的形参作为实参的别名。 形参和实参都是指向同一块内存位置的名称。
输出参数的要求有:
在方法内部,输出参数在能够被输出之前必须被赋值。 这意味着这与形参的初始值是无关的,而且没有必要在方法调用之前为实参赋值。
在方法返回之前, 方法内部贯穿的任何可能路径都必须为所有输出参数进行一次赋值。
● 因为在函数的体内我们需要读取输出变量之前必须对其写入,所以不可能使用输出参数把数据传入方法,
事实上,如果函数中有任何路径试图在输出参数被函数赋值之前读取它,就会发生错误。
namespace Ch05Ex03
{
class MyClass
{
public int Val = 20;
}
class Program
{
static void RefAsParameter(out MyClass f1,out int f2)
{
f1 = new MyClass();
f1.Val = 25;
f2 = 15;
WriteLine($"在函数中输出f1.va1的值和 f2的值:{f1.Val} , {f2}");
}
static void Main(string[] args)
{
MyClass a1 = null;
int a2;
RefAsParameter(out a1,out a2);
WriteLine($"在主函数中输出a1.va1的值和 a2的值:{a1.Val} , {a2}");
ReadKey();
}
}
}
输出结果为:
在函数中输出f1.va1的值和 f2的值:25 , 15
在主函数中输出f1.va1的值和 f2的值:25 , 15
参数数组
● 参数数组允许零个或者多个实参对应一个特殊的形参,重点如下:
在一个参数列表中只能有一个参数数组。而且必须是最后一个。
由参数数组表示的所有参数都必须具有相同的类型。
● 注意: 数组也是一个引用类型,所以它的所有数据项都保存在堆中。
可以使用两种方式为参数数组提供实参,下面我们来看一下:
namespace Ch05Ex03
{
class Program
{
static void ListInits(params int[] inVals)
{
foreach(var arr in inVals)
{
WriteLine(arr);
}
WriteLine();
}
static void Main(string[] args)
{
//第一种初始化方式,又是被称为延伸式,这种形式在调用中使用分离的实参。
ListInits(10, 20, 30); //直接列表初始化,而且所有元素必须是函数中声明指定的类型。
int[] inArray = { 11, 22, 33, }; //第二种初始化方式, 一个该数据类型元素的一维数组
ListInits(inArray);
ReadKey();
}
}
}
● 在使用第一种方式为参数熟数组分离实参的调用时,编译器会做以下的工作:
接受实参列表,用它们在堆中创建并初始化一个数组。
把数组的引用保存在栈中的形参里。
如果在对应数组的形参中没有实参,编译器会创建一个有零个元素的数组来使用。
na
namespace Ch05Ex03
{
class Program
{
static void ListInits(params int[] inVals)
{
if (inVals != null && inVals.Length!=0 )
{
for(int i=0; i<inVals.Length;++i)
{
inVals[i] = inVals[i] * 10;
WriteLine($"{inVals[i]}");
}
}
}
static void Main(string[] args)
{
int first = 5, second = 6, third = 7;
ListInits(first, second, third);
WriteLine($"{first}, {second}, {third}");
ReadKey();
}
}
}
输出结果为:
50
60
70
5, 6, 7
● 注意: 该程序中当数组在堆中被创建时, 实参的值被复制到数组中。 采用的是值传递的方式。
● 如果数组参数是采用值传递时,那么值被复制,实参不受函数内部的影响。
如果数组参数采用的是引用类型传递,那么引用被复制,实参引用的对象可以受到函数内部的影响。
命名参数
● 如果在在函数中调用函数,该函数的实参必须跟形参的位置一一对应。
● 命名参数指的是只要在实参列表中指定参数的名字,同时后面跟一个 :符号,在在写一个实参值(也可以是表达式),这样我们就可以在调用函数中以任意顺序列出实参。
namespace Ch05Ex03
{
class Program
{
static int ListInits(int a,int b,int c)
{
return (a + b) * c;
}
static void Main(string[] args)
{
int result = ListInits(c: 2, a: 4, b: 3); // 这样使用命名参数来传递实参
WriteLine($"输出result的结果:{result}");
ReadKey();
}
}
}
在调用函数中我们既可以使用位置参数也可以使用命名参数,但注意的是,所有的位置参数必须先写在实参表的前面。
namespace Ch05Ex03
{
class Program
{
static int ListInits(int a,int b,int c)
{
return (a + b) * c;
}
static void Main(string[] args)
{
int result = ListInits(c: 2, a: 4, b: 3);
WriteLine($"输出result的结果:{result}");
WriteLine($"采用位置参数传递:{ListInits(5, 7, 8)}");
WriteLine($"采用位置参数和命名参数传递:{ListInits(2, c: 6, b: 7)}"); //这里是位置参数和命名参数 混合使用
WriteLine($"采用表达式传递:{ListInits(c: 2, a: 5 + 5, b: 7 + 7)}");
ReadKey();
}
}
}
可选参数
● 对于默认值形参的声明,需要注意的有:
不是所有的数据类型都可以作为默认值形参的数据类型。
只要值类型的默认值可以在编译时确定,就可以使用值类型作为函数的默认值形参。
只有在默认值是null的时候, 引用类型才可以作为默认值形参来使用。
注意:如果有params 参数,必须在默认值参数之后声明。